Introducing Qt Quick 3D Particles

As you may have already seen or heard, Qt Quick 3D introduces support for 3D particles in Qt 6.1. Similarly to instanced rendering , also 3D particles module is a technology preview in Qt 6.1 and will be fully supported in Qt 6.2. In this blog post we'll go through things every developer & designer should know about the new 3D particles, so please continue reading. With the visual elements such as particles, it is always beneficial to actually see what you can do with them. The video below goes through some demos from our Testbed application, included as part of the Qt 6.1 examples.

March ’21 – monthly digest

Blogs

Using Qt datatypes in Standard Library unordered containers

by Giuseppe D’Angelo

In the previous blog post about qHash, we explained:

  • how to declare an overload of qHash for your own datatypes;
  • how to hash Qt datatypes for which Qt does not provide a qHash overload; and
  • why you can’t use a type from the Standard Library (or just another library) as a key in QHash (newsflash: this has changed! More on it later).

In this post, we’ll continue our discussion regarding hashing functions, tackling a specific problem, namely, how to use a Qt datatype as a key of a Standard Library unordered associative container.

Read on…

Kann das nicht der Compiler machen?

Maßgeschneidertes C/C++ Tooling mit Clang

by Anton Kreuzkamp and Kevin Funk

Die Autoren erklaren in diesem Blog, wie es möglich ist, mithilfe von Clang (ein C/C++ Compiler), eine auf ihre Bedürfnisse angepasste, statische Codeanalyse zu entwickeln.

Um so wenig Rechenzeit wie nur möglich in anspruch zu nehmen, nutzt er den Low Level Code, welcher automatisch mit beistand von dem Compiler überprüft und umgeschrieben werden.

Lesen Sie den Blog…

Efficient custom shapes in QtQuick: shaders

by James Turner

In his previous post on this subject, James talked about creating the geometry for a custom sector item and making a perfectly curved edge for our sector, and much more.

This post is about writing shaders and QtQuick scene graph materials and creating a foolproof API to give to UX designers that won’t break when they get creative and add stuff you might not have thought of.

Read the blog…

Running Qt Without a GPU

by Zeno Endemann

Our partner Toradex hosted a blog of Zeno’s we thought you’d like to read:

Is it possible to get complex graphical software like Qt running smoothly on a small, economical device like the Toradex Colibri iMX6ULL? Yes – it sure is.

Read on…

 


KDAB TV

6 Things to consider: Before switching to Qt 6

Giuseppe D’Angelo offers 6 tips that will save you time and give you best practices when you’re ready to switch, as well as the impact Qt 6 will have on your applications if you choose not to.

Watch the video…

Kuesa 3D Studio Designer Workflow – Parts 2 and 3 released

Last month we introduced you to Part 1. Parts 2 and 3 complete the series of Kuesa 3D Studio videos by designers for designers.

Part 2 – Kirsi Sutherland shows how to import 3D into QML, how to test it, and make it interactive.

Part 3 – Nuno Pinheiro shows how to make final adjustments to create a more believable reflection.

See the full Kuesa playlist here…

KUESA 3D Mesh Instantiator API – for developers in Qt 3D

Paul Lemire shows how this often-overlooked feature in Qt 3D makes it easier for developers to leverage Qt 3D’s instanced drawing.

Watch the video here…

Introduction to QML – Module 9

This is the last module of our acclaimed QML series. The first in the series of 4 in Module 9 tells you about the basics of model/view from the C++ level and comes out of KDAB’s extensive training material. You can see it here.

It took us just over a year to create Introduction to Qt/QML, at a rate of about one module a month – 54 videos in all. Big thanks to our viewers for all the support, feedback, and questions during the year.

This isn’t the end for Qt/QM, though. KDAB will be back in due course with more tips to make your coding life more efficient and more fun, meanwhile…

Watch the video…

Qt Widgets and More – Updates

There are three additions to the Qt Widgets and More series for your edification and delight:

Avoiding QVariant::fromValue around your Own Types Document Templates in Qt Creator – Part 1 Document Templates in Qt Creator – Part 2


Trainings

Updates to KDAB Training

Advanced QML Online (Europe), 15 – 17 June

We’ve added an extra Advanced QML training to our schedule. Sign up while seats last. Date changes

Testing Qt with Squish Online (USA), May 3 – 7 (was May 17 – 21) Sign up…

Formation Qt Widgets Online (in French), Sept 21 – 23 (was Sept 28 – 30) Sign up…

Modern C++ – C++11/C++14/C++17, (Berlin), Oct 26 – 29 (was Sept 28 – 30) Sign up…

 


Events

Qt Embedded Days

KDAB is hosting this new, online, free event next month! We had a great response to our Call for Papers. Thank you to all who contributed!

In-depth technical talks across two days, from a wide range of speakers. What’s not to like! We’ll have the program sorted soon, but meantime you can see the abstracts and the speakers.

Check out the speakers…

Other Events coming up

Qt Desktop Days – May 19 – 20

As previously announced, we’re back, with our acclaimed online Qt event. The CfP is open now. Contribute a talk before April 25th! Find out more and sign up…

 

Qt Day Italy – all year

This year, QtDay 2021 offers a series of online events focused on Qt across the year.

 

Qt Dev/Des Days – May 18 – 21

After we announced our Qt Desktop Days, The Qt Company announced this event for designers. Unfortunate timing, but these things happen and, as they’re both online, it should be possible to catch the recorded talks later if you’re interested in both.

For KDAB’s plentiful offerings for Designer-Developers, see www.kuesa.com and acute.kdab.com as well as the videos mentioned above.

The post March ’21 – monthly digest appeared first on KDAB.

The Qt Company expands offering into quality assurance tools with acquisition of froglogic GmbH

We are excited to announce that The Qt Company has acquired a long time Qt partner froglogic. froglogic GmbH is a global leader in the software test automation market, providing state-of-the-art solutions to enhance software quality in any industry context. They have been a cornerstone in the Qt ecosystem for a long time, and we are happy to join forces with them and welcome their team in Hamburg & globally joining the Qt team!

A new face for the Qt Project

Qt has always been open-source. In 2011 the Qt Project was established to provide an improved contribution experience and open governance for Qt. This has been a great success and many valuable contributions to Qt are coming from outside of The Qt Company. 

PyQt6 Book now available: Create GUI Applications with Python & Qt6 — The hands-on guide to making apps with Python

Hello! Today I have released the first PyQt6 edition of my book Create GUI Applications, with Python & Qt6.

This update follows the 4th Edition of the PyQt5 book updating all the code examples and adding additional PyQt6-specific detail. The book contains 600+ pages and 200+ complete code examples taking you from the basics of creating PyQt applications to fully functional apps.

To celebrate the milestone, the book is available this week with 20% off. As with earlier editions, readers get access to all future updates for free -- so it's a great time to snap it up! You'll also get a copy of the PyQt5, PySide6 and PySide2 editions.

PyQt6 book cover

If you bought a previous edition of the book (for PyQt5, PySide2 or PySide6) you get this update for free! Just log into your account on LearnPyQt and you'll find the book already waiting for you under "My Books & Downloads".

If you have any questions or difficulty getting hold of this update, just get in touch.

Enjoy!

See the complete PyQt5 tutorial, from first steps to complete applications with Python & Qt5.

Qt for MCUs 1.8 released

A new feature update of Qt for MCUs is now available. Download version 1.8 to get access to additional MCU platforms, more options to limit the memory footprint, new APIs to create advanced and scalable user interfaces, and a solution to simplify the integration of Qt into existing projects.

Fully Digital Automotive HMIs Now Available for Everyone

HVAC concept on a single MCU

As an Automotive HMI crew, we wonder how to make the digital experience available for everyone. Our objective was to define the automotive HMI solution of a fully digital dashboard from premium cars in a much simpler and cost-effective way.

We focused on modern HMI UI and automotive components by using a single MCU. Nowadays, automotive hardware has rapidly become more powerful. In parallel, we can now use new tools for the rapid development of production-ready interfaces.

Teamed-up with Qt & NXP, we created the HVAC demo in a real case scenario for the automotive market. The demo is using dual-core architecture, dedicated GPU acceleration, and shared memory. All that gives a constant 60 fps on the newest NXP board with Qt for MCUs technology.

Looking more closely at the demo, we were focused on two main goals:

1. Create a concept for fully digital HVAC with rich & smooth UI graphics for the passengers.

The aim here was to achieve a high level of rendering performance using particular textures: static textures, transformed textures, and other animations. The demo uses the integration with a dedicated 2D GPU utilizing VGLite on the new NXP board (mimxrt1170) with Qt for MCUs.

2. Define the cost-effective and straightforward automotive safety-critical architecture.

We used the NXP i.MX RT1170 board features with dual-core architecture and the secondary core possibilities. This approach aims to have a distributed application responsibility and possibly safety-related parts on a single MCU. Our approach was to leverage the secondary core Cortex-M4 and run it next to Cortex-M7. The demo architecture is also including CAN communication and inter-core-communication with shared memory. As a next step, we plan to use hardware layers by using Qt for MCUs.

Based on our craft with the HVAC demo with dual-core architecture and new technology enablers, we proved that cost-effective automotive HMIs could be available for everyone!

The current state of the demo is the first step to develop our vision for innovative HMIs. With dual-core architecture & CAN communication, we defined a dedicated controller for the HVAC. The primary function of the KNOB controller is taking care of advanced functionality for passengers’ comfort configuration.

HVAC & KNOB concept

Navigation of the KNOB controller is based on swiping the screens, pushing, and rotating the unit. All screens will be adjusted based on the context. This approach is giving interesting possibilities for passengers. Please see below how we see it as the early concept.

KNOB navigation

This is an example of how we envision these gestures in the next-gen cars that appear very soon on the market, so let’s be in touch with our next steps!

To try for free, visit the demo images for Qt for MCUS on NXP hardware and ask your burning questions in our upcoming live events with interactive Q&As!

How tu customise installer behaviour with Qt Installer Framework

In previous posts of this series, we briefly touched on the topic of scripting, while describing how to add custom pages to the installer UI. In this entry we will go much deeper. You will be able to learn what types of scripts are available and how to create them in the Qt Installer Framework – all of that with some handy examples. Let’s go!

 

API overview

Before doing the scripting itself, let’s explain what is the Scripting API and how it works.

In the Qt Installer Framework, Scripting API is basically a set of JavaSctipt objects. Scripts created with this API use files with .qs extension, which are executed by QJSEngine. You can use scripts to execute certain actions or modify the installer behaviour during runtime, for example by adding custom pages depending on which components were selected for installation. In the case of installers, there are two main types of scripts: controller scripts and component scripts.

Controller scripts are used for interacting with certain parts of the installer’s UI or functionality. For instance, you can modify existing pages or simulate user clicks in order to automate some parts of the installation. For each installer, there can be only a single controller script that is pointed in the config.xml file.

Component scripts are used to handle behaviour for selected component in the installer, so each component can have its own script. Because of that, each installer can contain several component scripts. They are executed when the component is selected for installation. The component script you want to use have to be pointed in the package.xml file.

Controller script basics

First, let’s talk a little bit about scripts structure in Qt Installer Framework. Both controller and component scripts share almost the same structure – the only difference between them is based on what you can do inside the script and the name of the constructor.

A minimal valid script needs to contain at least a constructor. For beginning we can create script.qs file. In order to make this file be recognized as proper script we have to put the empty constructor in it. In the case of the controller script, it is the following constructor:

function Controller()
{
}

Now let’s implement some simple function to modify installer behaviour. Let’s say that we want to skip some pages in the installer automatically – for example, the introduction page. One of the simple solutions to do this is to invoke a click on the „Next” button when the selected page appears. To do so, we can use Controller.prototype.IntroductionPageCallback. The script should look as follows:

function Controller()
{
}

Controller.prototype.IntroductionPageCallback = function()
{
    gui.clickButton(buttons.NextButton);
}

In this script, we used two global objects that are accessible in controller scripts. The first one is the gui which is JavaScript global object that allows for interaction with the installer UI. The buttons, as the name implies, allow us to use buttons placed in installer pages. Besides those objects, each page has a set of its own methods, buttons and widgets. For example on component selection page you can use functions like selectAll() or selectComponent(id) in order to manage component selection. We won’t describe all of them in this post as it would take too long. If you want to get familiar with all available options, take a look at the documentation.

Now in order to use script in your installer, open configuration file for your installer and simply add the following tag to it:

<ControlScript>script.qs</ControlScript>

Don’t forget to place script.qs inside the configuration directory! Now your first controller script is hooked up – you can generate a new installer and give it a try. If you don’t remember how to do it, take a look at the first post of this series.

Component script basics

Now we have a functional controller script. Let’s create the basic component script. Just like in the case of controller script you have to start with a constructor – in this case, its name is Component. As a simple example of a component script, let’s use the one from previous post. Its purpose is to add custom page to the installer if the component is selected. To implement it we can use addWizardPage function:

function Component()
{
    installer.addWizardPage(component, "MyOtherPage", QInstaller.ReadyForInstallation);
}

A function that adds new page takes three parameters. First is the component which contains the registered name of the widget class you want to add – in this case, a global component object referencing the current Component was passed. The next argument is the name of the registered widget class you want to use. Last is the index of the existing page before which new page will be placed. In this case, CustomPage would be placed before the ReadyForInstallation page.

In case to make this script working you also have to add a page .ui file that will contain page design in the package directory while also register this file in package.xml. Besides that you also need to point the script in the package.xml to make it execute:

<Script>installscript.qs</Script>
<UserInterfaces>
    <UserInterface>CustomPage.ui</UserInterface>
</UserInterfaces>

If you want to learn more about customizing installer UI, take a look at following post.

Adding a shortcut 

As you are now familiar with the basics of writing scripts, we can now move on to more advanced use cases with examples. Let’s say we have a Windows app installer and we want to create an app shortcut on the desktop after finishing an installation. To implement such functionality we can use the component script as different executables can be used for different packages.

All we need to do is to add custom operation that will be executed when the component is installed. To do so we can use Component.prototype.createOperations hook with addOperation function:

function Component()
{
    // default constructor
}

Component.prototype.createOperations = function()
{
    try {
        // call the base create operations function
        component.createOperations();
        // check if we are on widnows machine
        if (installer.value("os") == "win") {
            try {
                // look for user profile directory
                var userProfile = installer.environmentVariable("USERPROFILE");
                installer.setValue("UserProfile", userProfile);
                component.addOperation("CreateShortcut", "@TargetDir@\\win64\\application.exe", "@UserProfile@\\Desktop\\My\ App.lnk");                
            } catch (e) {
                // Do nothing if key doesn't exist
            }
        }
    } catch (e) {
        print(e);
    }
}

Recursive installation 

There might be a case that despite libraries and other files that you can simply place in the deployment folder there might be some external tools that are needed to make your app working on the end device. A most common example of such a case are Microsoft Visual C++ Redistributable packages containing C and C++ runtime, that are needed on the device if you compiled an app using an MVSC kit.

You can simply place runtime .dll files in the deployment folder however, there is a catch. Antivirus software doesn’t really like using C and C++ runtime libraries that were not installed with Microsoft installer, as modified runtime can do some nasty things in the user’s computer. Using this approach you can make your app be recognized as malicious, and you never want such a thing to happen. What should you do?

Solution is quite simple – perform recursive installation of redistributable packages. It may sound fancy and hard to implement, but it all comes down to check if there are existing VC runtime on the device and, if not, executing redistributable packages installer from Microsoft. Let’s take a look at how it is done.

The first thing you need to do is download a redistributable packages installer from the Microsoft website. After that place it in data folder of the package.

Now let’s go with the scripting. As we are working with a single component, we need a component script. First, we need to crate a custom function that will allow us to check for existing VC runtime and execute installer if it is missing:

Component.prototype.installVCRedist = function()
{
    var registryVC2017x64 = installer.execute("reg", new Array("QUERY", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\x64", "/v", "Installed"))[0];
    var doInstall = false;

    if (doInstall)
    {
        QMessageBox.information("vcRedist.install", "Install VS Redistributables", "The application requires Visual Studio 2017 Redistributables. Please follow the steps to install it now.", QMessageBox.OK);
        var dir = installer.value("TargetDir");
        installer.execute(dir + "/VC_redist.x64.exe", "/norestart", "/passive");
    }
}

In this function, we first check system registers to verify if there is VC runtime installed. If the installation is needed, a message box with proper information is shown. After clicking ok, the installer is executed. We are using the „TargetDir” path as the VC runtime installer is placed in the same place as the root directory of the installed app.

All is left to do is to execute the function after installation of the component is finished. To do so we can use signal and slots system that is also available in the script engine:

function Component()
{
    // constructor
    installer.installationFinished.connect(this, Component.prototype.installVCRedist);
}

Summary 

In this post, you learned what is Scripting API in Qt Installer Framework and how to use it.  Using this fresh knowledge you can now play and experiment with the installer scripts as the possibilities for applying them are endless. There are many more popular use cases for scripts than those presented in this post. In future posts we will present more of them, so stay tuned.

If you like this post subscribe to our newsletter and stay up to date.

Qt 6.0.3 Released

We have released Qt 6.0.3 today. As a  patch release the Qt 6.0.3 does not add any new functionality but provides bug fixes and other improvements.

Creating Advanced HMIs & User Experiences for NXP i.MX RT1170

For some time now, the challenges of developing advanced human machine interfaces (HMIs) for embedded platforms has centered around translating the design vision to code, and then deploying that code to hardware platforms that meet the cost sensitivities of the market. The trend on user preferences around HMI, spanning across all industries, is tied to the benchmark most of us carry right in our pocket daily – the smartphone. When a user is met with a screen, it is an almost automatic expectation that the experience be the same as on their smartphone, regardless of what is under the hood. This can be an extreme challenge for cost sensitive markets where the budget for differentiation is low. Consumer markets are showing increased interest in advanced user interface technologies that can be effective at putting yourself ahead of the competition. Regardless of your market’s cost sensitivity, a premium user experience should not require a higher complexity and cost in your Bill of Materials (BOM). NXP™ Semiconductor’s recent release of the i.MX RT1170 crossover microcontroller (MCU) ushers in the era of GHz performance, and when paired with the complete software technology, enables an exceptional user experience at a cost suited to markets across many industries.

Qt Desktop Days Returns!

KDAB is bringing you another Qt Desktop Days this May! So, if you missed the first one that was held last September, or if you would like to attend a second, please join us May 19th-20th. That’s a little less than two months away!

Qt Desktop Days is primarily a technical event and we want our content to be as relevant and interesting as possible to our audience. The target audience is developers programming with Qt, building software for desktop systems.

Call for Papers

The Call for Papers, or CFP, is now open!

We welcome proposals related to any aspect of Qt desktop software development, as long as they:

  • Bring practical knowledge for the attendees
  • Discuss the unique technical challenges of creating desktop applications
  • Are about Qt and its ecosystem.

The goal of the conference is to make the attendees better Qt programmers. Submissions are due by April 25.

For more information about the Call For Papers, visit the Call For Talks page of our Qt Desktop Days website.

Registration

Sign up to receive program updates and to get the event link in time for the event.

Find out more about this event, anytime, by perusing our website devoted to Qt Desktop Days.

qt desktop days in may

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 Desktop Days Returns! appeared first on KDAB.

PySide6 Book now available: Create GUI Applications with Python & Qt6 — The hands-on guide to making apps with Python

Hello! This morning I released the first Qt6 edition of my PySide book Create GUI Applications, with Python & Qt6.

This update follows the 4th Edition of the PySide2 book updating all the code examples and adding additional PySide6-specific detail. The book contains 600+ pages and 200+ complete code examples taking you from the basics of creating PySide applications to fully functional apps.

To celebrate the milestone, the book is available this week with 20% off. As with earlier editions, readers get access to all future updates for free -- so it's a great time to snap it up! You'll also get a copy of the PyQt5 and PySide2 editions.

PySide6 book cover

If you previously bought a copy of my Qt5 books (for PyQt5 or PySide2) you get this update for free! Just log into your account on LearnPyQt and you'll find the book already waiting for you under "My Books & Downloads".

The PyQt6 edition will be released shortly. If you have any questions or difficulty getting hold of this update, just get in touch.

Enjoy!

See the complete PyQt5 tutorial, from first steps to complete applications with Python & Qt5.

PyQt6 vs PySide6: What's the difference between the two Python Qt libraries? — ...and what's exactly the same (most of it)

There is a new version of Qt (version 6) and with it new versions of PyQt and PySide -- now named PyQt6 & PySide6 respectively. In preparation for the Qt6 editions of my PyQt5 & PySide2 books I've been looking at the latest versions of the libraries to identify the differences between them and find solutions for writing portable code.

Interested in a PyQt6 or PySide6 book? The new PySide6 edition and PyQt6 edition of Create GUIs with Python & Qt are now available! If you buy either book, you receive both editions (and the PyQt5 and PySide2 editions too).

In this short guide I'll run through why there are two libraries, whether you need to care (spoiler: you don't), what the differences are and how to work around them. By the end you should be comfortable re-using code examples from both PyQt6 and PySide6 tutorials to build your apps, regardless of which package you're using yourself.

Background

Why are there two libraries?

PyQt is developed by Phil Thompson of Riverbank Computing Ltd. and has existed for a very long time — supporting versions of Qt going back to 2.x. In 2009 Nokia, who owned Qt toolkit at the time, wanted to make the Python bindings for Qt available in a more permissive LGPL license.

It's called PySide because "side" is Finnish for "binder".

The two interfaces were basically equivalent, but over time development of PySide lagged behind PyQt. This was particularly noticeable following the release of Qt 5 — the Qt5 version of PyQt (PyQt5) has been available since mid-2016, while the first stable release of PySide was 2 years later. However, the Qt project has recently adopted PySide as the official Qt for Python release which should ensure its viability going forward. When Qt6 was released, both Python bindings were available shortly after.

PyQt6 PySide6
First stable release Jan 2021 Dec 2020
Developed by Riverbank Computing Ltd. Qt
License GPL or commercial LGPL
Platforms Python 3 Python 3

Which should you use? Well, honestly, it doesn't really matter.

Both packages are wrapping the same library — Qt6 — and so have 99.9% identical APIs (see below for the few differences). Anything you learn with one library will be easily applied to a project using the other. Also, no matter with one you choose to use, it's worth familiarizing yourself with the other so you can make the best use of all available online resources — using PyQt6 tutorials to build your PySide6 applications for example, and vice versa.

In this short chapter I'll run through the few notable differences between the two packages and explain how to write code which works seamlessly with both. After reading this you should be able to take any PyQt6 example online and convert it to work with PySide6.

Licensing

The main notable difference between the two versions is licensing — with PyQt6 being available under a GPL or commercial license, and PySide6 under a LGPL license.

If you are planning to release your software itself under the GPL, or you are developing software which will not be distributed, the GPL requirement of PyQt6 is unlikely to be an issue. However, if you want to distribute your software but not share your source code you will need to purchase a commercial license from Riverbank for PyQt6 or use PySide6.

Qt itself is available under a Qt Commercial License, GPL 2.0, GPL 3.0 and LGPL 3.0 licenses.

Namespaces & Enums

One of the major changes introduced for PyQt6 is the need to use fully qualified names for enums and flags. Previously, in both PyQt5 and PySide2 you could make use of shortcuts -- for example Qt.DecorationRole, Qt.AlignLeft. In PyQt6 these are now Qt.ItemDataRole.DisplayRole and Qt.Alignment.AlignLeft respectively. This change affects all enums and flag groups in Qt. In PySide6 both long and short names remain supported.

The situation is complicated somewhat by the fact that PyQt6 and PySide6 use subtly different naming conventions for flags. In PySide6 (and v5) flags are grouped under flag objects with the "Flag" suffix, for example Qt.AlignmentFlag -- the align left flag is Qt.AlignmentFlag.AlignLeft. The same flag group in PyQt6 is named just "Qt.Alignment". This means that you can't simply choose long or short form and retain compatibility between PyQt6 & PySide6.

UI files

Another major difference between the two libraries is in their handling of loading .ui files exported from Qt Creator/Designer. PyQt6 provides the uic submodule which can be used to load UI files directly, to produce an object. This feels pretty Pythonic (if you ignore the camelCase).

python
import sys
from PyQt6 import QtWidgets, uic

app = QtWidgets.QApplication(sys.argv)

window = uic.loadUi("mainwindow.ui")
window.show()
app.exec()

The equivalent with PySide6 is one line longer, since you need to create a QUILoader object first. Unfortunately the API of these two interfaces is different too (.load vs .loadUI).

python
import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtUiTools import QUiLoader

loader = QUiLoader()

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

To load a UI onto an existing object in PyQt6, for example in your QMainWindow.__init__ you can call uic.loadUI passing in self(the existing widget) as the second parameter.

python
import sys
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6 import uic


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        uic.loadUi("mainwindow.ui", self)


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

The PySide6 loader does not support this — the second parameter to .load is the parent widget of the widget you're creating. This prevents you adding custom code to the __init__ block of the widget, but you can work around this with a separate function.

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

loader = QUiLoader()

def mainwindow_setup(w):
    w.setWindowTitle("MainWindow Title")

app = QtWidgets.QApplication(sys.argv)

window = loader.load("mainwindow.ui", None)
mainwindow_setup(window)
window.show()
app.exec()

Converting UI files to Python

Both libraries provide identical scripts to generate Python importable modules from Qt Designer .ui files. For PyQt6 the script is named pyuic5

bash
pyuic6 mainwindow.ui -o MainWindow.py

You can then import the UI_MainWindow object, subclass using multiple inheritance from the base class you're using (e.g. QMainWIndow) and then call self.setupUI(self) to set the UI up.

python
import sys
from PyQt6 import QtWidgets
from MainWindow import Ui_MainWindow

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):

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


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

For PySide6 it is named pyside6-uic

bash
pyside6-uic mainwindow.ui -o MainWindow.py

The subsequent setup is identical.

python
import sys
from PySide6 import QtWidgets
from MainWindow import Ui_MainWindow

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):

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


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

exec() or exec_()

The .exec() method is used in Qt to start the event loop of your QApplication or dialog boxes. In Python 2.7 exec was a keyword, meaning it could not be used for variable, function or method names. The solution used in both PyQt4 and PySide was to rename uses of .exec to .exec_() to avoid this conflict.

Python 3 removed the exec keyword, freeing the name up to be used. As a result from PyQt6 .exec() calls are named just as in Qt. However, PySide6 still uses .exec_().

Slots and Signals

Defining custom slots and signals uses slightly different syntax between the two libraries. PySide6 provides this interface under the names Signal and Slot while PyQt6 provides these as pyqtSignal and pyqtSlot respectively. The behavior of them both is identical for defining and slots and signals.

The following PyQt6 and PySide6 examples are identical —

python
my_custom_signal = pyqtSignal()  # PyQt6
my_custom_signal = Signal()  # PySide6

my_other_signal = pyqtSignal(int)  # PyQt6
my_other_signal = Signal(int)  # PySide6

Or for a slot —

python
@pyqtslot
def my_custom_slot():
    pass

@Slot
def my_custom_slot():
    pass

If you want to ensure consistency across PyQt6 and PySide6 you can use the following import pattern for PyQt6 to use the Signal and @Slot style there too.

python
from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

You could of course do the reverse from PySide6.QtCore import Signal as pyqtSignal, Slot as pyqtSlot although that's a bit confusing.

QMouseEvent

In PyQt6 QMouseEvent objects no longer have the .pos(), .x() or .y() shorthand property methods for accessing the position of the event. You must use the .position() property to get a QPoint object and access the .x() or .y() methods on that. The .position() method is also available in PySide6.

Features in PySide6 but not in PyQt6

As of Qt 6 PySide supports two Python __feature__ flags to help make code more Pythonic with snake_case variable names and the ability to assign and access properties directly, rather than using getter/setter functions. The example below shows the impact of these changes on code --

python
table = QTableWidget()
table.setColumnCount(2)

button = QPushButton("Add")
button.setEnabled(False)

layout = QVBoxLayout()
layout.addWidget(table)
layout.addWidget(button)

The same code, but with snake_case and true_property enabled.

python
from __feature__ import snake_case, true_property

table = QTableWidget()
table.column_count = 2

button = QPushButton("Add")
button.enabled = False

layout = QVBoxLayout()
layout.add_widget(table)
layout.add_widget(button)

These feature flags are a nice improvement for code readability, however as they are not supported in PyQt6 it makes writign portable code more difficult.

Supporting both in libraries

You don't need to worry about this if you're writing a standalone app, just use whichever API you prefer.

If you're writing a library, widget or other tool you want to be compatible with both PyQt6 and PySide6 you can do so easily by adding both sets of imports.

python
import sys

if 'PyQt6' in sys.modules:
    # PyQt6
    from PyQt6 import QtGui, QtWidgets, QtCore
    from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

else:
    # PySide6
    from PySide6 import QtGui, QtWidgets, QtCore
    from PySide6.QtCore import Signal, Slot

This is the approach used in our custom widgets library, where we support for PyQt6 and PySide6 with a single library import. The only caveat is that you must ensure PyQt6 is imported before (as in on the line above or earlier) when importing this library, to ensure it is in sys.modules.

To account for the lack of shorthand enum and flags in PyQt6 you can generate these yourself. For example, the following code will copy references for each of the enum objects elements up to their parent object, making them accessible as in PyQt5, PySide2 & PySide6. The code would only need to be run under PyQt6.

python
enums = [
    (QtCore.Qt, 'Alignment'),
    (QtCore.Qt, 'ApplicationAttribute'),
    (QtCore.Qt, 'CheckState'),
    (QtCore.Qt, 'CursorShape'),
    (QtWidgets.QSizePolicy, 'Policy'),
]

# Look up using the long name (e.g. QtCore.Qt.CheckState.Checked, used
# in PyQt6) and store under the short name (e.g. QtCore.Checked, used
# in PyQt5, PySide2 & accepted by PySide6).
for module, enum_name in enums:
    for entry in getattr(module, enum_name):
        setattr(module, entry.name, entry)

Alternatively, you can define a custom function to handle the namespace lookup.

python
def _enum(obj, name):
    parent, child = name.split('.')
    result = getattr(obj, child, False)
    if result:  # Found using short name only.
        return result

    obj = getattr(obj, parent)  # Get parent, then child.
    return getattr(obj, child)

When passed an object and a PyQt6 compatible long-form name, this function will return the correct enum or flag on both PyQt6 and PySide6.

python
>>> _enum(PySide6.QtCore.Qt, 'Alignment.AlignLeft')
PySide6.QtCore.Qt.AlignmentFlag.AlignLeft
>>> _enum(PyQt6.QtCore.Qt, 'Alignment.AlignLeft')
<Alignment.AlignLeft: 1>

The final complication is the mismatch in the exec_() and exec() method calls. You can work around this by implementing a function to check the presence of each method and call whichever exists.

python
def _exec(obj):
    if hasattr(obj, 'exec'):
        return obj.exec()
    else:
        return obj.exec_()

You can then use this function to exec objects like QApplication and QDialog in a portable way on both PyQt6 and PySide6.

python
app = QApplication(sys.argv)
_exec(app)

If you're doing this in multiple files it can get a bit cumbersome. A nice solution to this is to move the import logic and custom shim methods to their own file, e.g. named qt.py in your project root. This module imports the Qt modules (QtCore, QtGui, QtWidgets, etc.) from one of the two libraries, and then you import into your application from there.

The contents of the qt.py are the same as we used earlier —

python
import sys

if 'PyQt6' in sys.modules:
    # PyQt6
    from PyQt6 import QtGui, QtWidgets, QtCore
    from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot


else:
    # PySide6
    from PySide6 import QtGui, QtWidgets, QtCore
    from PySide6.QtCore import Signal, Slot


def _enum(obj, name):
    parent, child = name.split('.')
    result = getattr(obj, child, False)
    if result:  # Found using short name only.
        return result

    obj = getattr(obj, parent)  # Get parent, then child.
    return getattr(obj, child)


def _exec(obj):
    if hasattr(obj, 'exec'):
        return obj.exec()
    else:
        return obj.exec_()

You must remember to add any other PyQt6 modules you use (browser, multimedia, etc.) in both branches of the if block. You can then import Qt6 into your own application as follows —

python
from .qt import QtGui, QtWidgets, QtCore, _enum, _exec

...and it will work seamlessly across either library.

That's really it

There's not much more to say — the two libraries really are that similar. However, if you do stumble across any other PyQt6/PySide6 examples or features which you can't easily convert, drop me a note.

See the complete PyQt5 tutorial, from first steps to complete applications with Python & Qt5.

How to use NFC in Qt/Qml application?

Have you ever wondered how to use NFC in Qt/Qml application? This blog post will give you a complex overview of the concept of near-field communication technology with examples of its usage in real life. Then you will discover how to implement NFC reading and writing in Qt Qml application.

 

 

 

What is NFC?

NFC stands for near–field communication and it provides a way to transfer data wirelessly. It evolved from RFID technology which used electromagnetic induction to transmit data. You could have a RFID chip in hand and probably you did not even know about it, as they are often used in form of pendant allowing to access for e.g. office space.

NFC, just like other wireless transfer technologies (Bluetooth, Wi-Fi), take advance of radio waves. However, the main advantage of NFC is the fact that passive devices do not require additional power supply. When there is a passive device in the discovery range of active device, the electromagnetic field created by active NFC devices powers the passive one. Also unlike Bluetooth, near-field communication does not require to first pair with other device.

As the name implies, NFC allows to transfer data within short-range which is roughly 10 centimeters (around 4 inches). This short range is often posed as an advantage, because of security reasons.

Most of today’s smartphones are equipped with NFC antennas being active NFC devices. What does this mean? Active devices allow us to both send and receive data by communicating with each other or with passive devices. Passive devices, on the other hand, can connect to neither active nor passive devices. This type of devices are usually used as interactive tags to hold some data.

 

3 NFC usage examples

Take a look to find out how NFC technology is used in daily life. Get inspired on how you can benefit from using NFC in your app.

Mobile payments with NFC

Both Android and iPhone devices employ NFC to perform secure, wireless payments which have become a common way to pay for goods in many countries across the world. With NFC you can use your phone just like a wireless credit card thanks to a built-in NFC antenna.

Many digital wallet platforms just like Google Pay, Apple Pay or Samsung Pay take advantage of NFC allowing you to use them together with this technology.

 

Sharing between two active devices with NFC

On Android you have the possibility to transfer data from one smartphone to another just by touching both devices back-to-back. After confirmation data are sent rapidly.

NFC tags

As mentioned, passive NFC devices do not need to have their own power source and they can be programmed to perform a wide range of tasks and to keep various kinds of data. Popular, yet simple usage of NFC tag is putting one next to bed and programming it to automatically enable silent mode when touched by phone.

Of course those are only few examples of how NFC technology is used. Number of possible usages of NFC tags is infinitive. Continue reading to take a look at usage implementation of NFC chips in Qt Qml/application.

How to implement NFC in your Qt/Qml Application?

Among many features shipped within the feature-rich Qt Framework, there is a Qt NFC module that allows to communicate over NFC with both active and passive devices. Currently the module supports Linux using Neard and Android. Let’s see how to implement NFC in your Qt Qml application.

First let’s start with an idea for mobile application that will take advantage of NFC tags. I want to have an application that will ease my cook’s life. It will allow users to put a dish name on a NFC tag together with time indicating how long cooking or baking takes. Then when such a tag is touched, the application will start a timer counting down from that time and then a sound alarm will be triggered. This way you will never burn your dishes.

Presented demo is simple, but good enough to show real adoption of NFC in Qt app, including:

  • NFC tags detection;
  • NFC tags writing;
  • NFC tags reading;
  • Exposing C++ objects and classes to Qml;

This blog post will not cover UI part done in Qml, so feel free to look at full code source that can be found on Scythe Studio’s Github profile.

NFC tags detection

Let’s start with the most important part of NFC implementation – detection. We created NFCManager class that is supposed to be responsible for all the NFC actions like detecting, reading and writing. It is exposed to Qml in an easy way thanks to QML_ELEMENT macro that was introduced in Qt 5.15.

Among other members and methods there is QNearFieldManager pointer that is crucial to control NFC targets detection. 

// directives, forward declarations
// Record struct declaration

class NFCManager : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool hasTagInRange READ hasTagInRange NOTIFY hasTagInRangeChanged)
    Q_PROPERTY(ActionType actionType READ actionType WRITE setActionType NOTIFY actionTypeChanged)
    Q_PROPERTY(Record record READ record NOTIFY recordChanged)
    QML_ELEMENT

public:
    explicit NFCManager(QObject *parent = nullptr);

    enum ActionType
    {
        None = 0,
        Reading,
        Writing
    };
    Q_ENUM(ActionType)

    bool hasTagInRange() const;
    ActionType actionType() const;
    Record record() const;

 

public slots:
    void startReading();
    void stopDetecting();
    void saveRecord(const QString &dishName, int seconds);

signals:
    void hasTagInRangeChanged(bool hasTagInRange);
    void actionTypeChanged(ActionType actionType);
    void recordChanged(const Record &record);

    void wroteSuccessfully();
    void nfcError(const QString &error);

private slots:
    void setActionType(ActionType actionType);
    void setHasTagInRange(bool hasTagInRange);

    void onTargetDetected(QNearFieldTarget *target);
    void onTargetLost(QNearFieldTarget *target);

    void onNdefMessageRead(const QNdefMessage &message);
    void onNdefMessageWritten();
    void handleTargetError(QNearFieldTarget::Error error, const QNearFieldTarget::RequestId &id);

private:
    bool m_hasTagInRange = false;
    ActionType m_actionType = ActionType::None;

    Record m_record;
    QNearFieldManager *m_manager;
    QNearFieldTarget::RequestId m_request;

};

In class constructor you need to initialize the QNearFieldManager instance and connect appropriate signals to be able to handle NFC events. The signals that we are interested in are `QNearFieldManager::targetDetected` and `QNearFieldManager::targetLost`. As names indicate, they are emitted when the target device (either active or passive) enters or leaves the detection range. 

NFCManager::NFCManager(QObject *parent)
    : QObject(parent)
    , m_manager(new QNearFieldManager(this)
{
    connect(m_manager, &QNearFieldManager::targetDetected,
            this, &NFCManager::onTargetDetected);

    connect(m_manager, &QNearFieldManager::targetLost,
            this, &NFCManager::onTargetLost);
}  

Signals will not be emitted unless we ask QNearFieldManager instance to start detection. To do that you need to call `QNearFieldManager::startTargetDetection` method on the object instance. Before starting detection set target access mode to tell QNearFieldManager what you are going to do with the target device – either read or write. For reading you need to set the target access mode to `QNearFieldManager::NdefReadTargetAccess` and for writing use `QNearFieldManager::NdefWriteTargetAccess`.

void NFCManager::startReading()
{
    setActionType(ActionType::Reading);
    m_manager->setTargetAccessModes(QNearFieldManager::NdefReadTargetAccess);
    m_manager->startTargetDetection();
}

void NFCManager::stopDetecting()
{
    setActionType(ActionType::None);
    m_manager->setTargetAccessModes(QNearFieldManager::NoTargetAccess);
    m_manager->stopTargetDetection();
}

The code snippet above shows you how to start detection for reading and how to stop detection regardless of current access mode. There is also `NFCManager::setActionType()` method used, but it is not part of Qt NFC API. It is used in this demo to indicate which action (Reading, Writing, None) is being performed now.

NFC Tags Writing

Once the detection part is done we need to implement NFCManager behavior when the tag enters and leaves range. First let’s implement behavior for writing tags to put some data on them. For this demo I used NFC tag stickers.

You may notice that the NFCManager has a `Record m_record` member. This variable is used to keep data to be written on tag or data read from a tag. `Record` is a structure, so let’s take a look at its declaration.

struct Record {
    Q_GADGET
    Q_PROPERTY(int seconds MEMBER seconds)
    Q_PROPERTY(QString dishName MEMBER dishName)

public:
    int seconds = 0;
    QString dishName = "";
    bool parseNdefMessage(const QNdefNfcTextRecord &record);
    QNdefMessage generateNdefMessage() const;
};
Q_DECLARE_METATYPE(Record)

The structure has two fields responsible for keeping the name of the dish and cooking time in seconds. To write Ndef messages on target device you need to generate QNdefMessage using data kept in a structure. QNdefMessage is a collection of QNdefRecords that are single parts of a message. The best way to implement NFC in Qt/Qml application is to make a derived class based on QNdefRecord. To keep this demo simple let’s assume that we will keep only one message with one record on one tag and the content of the tag will be a JSON string. 

QNdefMessage Record::generateNdefMessage() const
{
    if (dishName.isEmpty() || seconds <= 0) {
        return QNdefMessage();
    }

    QNdefMessage message; 

    QVariantMap recordMap{};
    recordMap[DISHNAME] = dishName;
    recordMap[SECONDS] = seconds;

    const QJsonDocument &doc = QJsonDocument::fromVariant(recordMap); 

    QNdefNfcTextRecord record;
    record.setEncoding(QNdefNfcTextRecord::Utf8);
    record.setText(doc.toJson());
    message.append(record);

    return message;
}

 

So this is how you could implement a method for generating QNdefMessage from your own structure. First JSON document is created and then it is set as text for QNdefNfcTextRecord variable. This record is then put in to QNdefMessage. 

void NFCManager::saveRecord(const QString &dishName, int seconds)
{
    m_record.dishName = dishName;
    m_record.seconds = seconds;

    setActionType(ActionType::Writing);
    m_manager->setTargetAccessModes(QNearFieldManager::NdefWriteTargetAccess);
    m_manager->startTargetDetection();
}

As a next step we’ll take a look at `NFCManager::saveRecord` implementation that is pretty similar to `NFCManager::startReading` method, but it has two extra parameters to save name of the dish and cooking time into m_record. Also it has different target access mode. This method is called from Qml part with parameters fetched from the user’s input

It’s time to implement an actual behavior on target detection. For this purpose we will take use of internal m_actionType member to decide what to do in particular situation as `QNearFieldManager::targetDetected` signal is emitted regardless of actual target access mode.

void NFCManager::onTargetDetected(QNearFieldTarget *target)
{
    setHasTagInRange(true);

    switch (m_actionType) {
    case None:
        break;
    case Reading:
        // reading ...
        break;
    case Writing:
        connect(target, &QNearFieldTarget::ndefMessagesWritten, this, &NFCManager::onNdefMessageWritten);
        connect(target, &QNearFieldTarget::error, this, &NFCManager::handleTargetError);

        m_request = target->writeNdefMessages(QList<QNdefMessage>() << m_record.generateNdefMessage());

        if (!m_request.isValid()) {
            handleTargetError(QNearFieldTarget::NdefWriteError, m_request);
        }
        break;
    }
}

From this point you can see that it is not complicated at all. All you need to do is to call `QNearFieldTarget::writeNdefMessages` on detected target with list of messages to be written on target as a parameter. For this demo list has only one single item that is a message generated from Record structure. 

To prevent silent errors you should validate the result of calling the method saved in m_request class member and connect it to `QNearFieldTarget::error`.

void NFCManager::onNdefMessageWritten()
{
    stopDetecting();
    m_request = QNearFieldTarget::RequestId();

    emit wroteSuccessfully();
}

However remember to connect to target’s `QNearFieldTarget::ndefMessagesWritten` signal in order to handle successful message writing. In such method feel free to stop detecting, empty m_request and emit a signal indicating that all worked as it should.

That’s all for writing to NFC tags in Qt app. Now call `NFCManager::saveRecord` method on NFCManager instance exposed to Qml and touch a tag with your phone. Dish name and cooking time should be written successfully on a tag.

NFC Tags Reading

We already have some data on tag – now it’s time for reading. In `NFCManager::onTargetDetected` definition, in proper switch case (previously commented) call `QNearFieldTarget::readNdefMessages()` on detected target and save a result in m_request class member. However, before doing this connect to `QNearFieldTarget::ndefMessageRead` in order to get content of the tag. Remember about handling errors cases.

        connect(target, &QNearFieldTarget::ndefMessageRead, this, &NFCManager::onNdefMessageRead);
        connect(target, &QNearFieldTarget::error, this, &NFCManager::handleTargetError);

        m_request = target->readNdefMessages();
        if (!m_request.isValid()) {
            handleTargetError(QNearFieldTarget::NdefReadError, m_request);
        }

For NFC tags reading `NFCManager::onNdefMessageRead` plays first fiddle. It comes with QNdefMessage as a parameter so first we need to iterate over records in that message and look for records of type QNdefNfcTextRecord. As you used this type to write the message, it’s the only type we are interested in. If a record of such type was parsed successfully by `Record::parseNdefMessage` method, let’s finish iteration, stop detection and emit a signal indicating that record changed. Then you can handle this signal in QML. 

That is all you need to know. To finish this tutorial formally let’s take a look how you could implement `Record::parseNdefMessage`.

bool Record::parseNdefMessage(const QNdefNfcTextRecord &record)
{
    const QJsonDocument &doc = QJsonDocument::fromJson(record.text().toUtf8());
    const QJsonObject &recordObject = doc.object();
 
    if (!recordObject.contains(DISHNAME) || !recordObject.contains(DISHNAME)) {
        return false;
    }

    dishName = recordObject[DISHNAME].toString();
    seconds = recordObject[SECONDS].toInt();

    return true;
}

As you see there is nothing exceptionaly difficult here. QNdefNfcTextRecord parameter has a text method that returns saved string. From that point you can parse it as json as usual. 

How to use NFC in Qt/Qml application – summary

Thanks for getting familiar with NFC in Qt with us. Thanks to this blog post you should:

  • Understand what is Near Field Communication technology;
  • Know what is the usage of NFC and how you can benefit from this;
  • Know how to implement detecting, writing and reading NFC targets in Qt/Qml application;

Efficient custom shapes in QtQuick : shaders

A long time ago, I wrote a post about creating custom shapes in Qt Quick, via the scene-graph APIs. That post covered defining suitable geometry to draw a part of a circle, known also as a ‘sector’, efficiently, since such sectors occur commonly in instrument and vehicle interfaces. I started writing the second part, about implementing the material and shaders to make the complete functioning item.

Then, somehow, life and customers intervened, and I forgot all about publishing the second part, and now it’s an embarrassingly long time later. Not as bad as the wait for A Dance With Dragons or Duke Nukem forever, but still, embarrassing. This is all my way of saying, here’s a post about writing shaders and QtQuick scene graph materials, which is interesting but also more than three years late, and that WordPress apparently doesn’t remind you that you have drafts sitting around.

In the previous post, I talked about creating the geometry for the custom sector item. We saw that I need to define the geometry as a collection of triangles (since GPUs love triangles), each triangle having three points, or vertices. Additionally, we saw that we can create our triangles somewhat larger than we need, to allow us to make a perfectly curved edge for our sector, as well as to permit some other effects such as anti-aliasing (reducing the appearance of steps or ‘jaggies’ on the edge) and even adding borders. And UX designers love borders almost as much as they love gradients and rounded corners.

Making our triangles larger also means we need fewer. The extreme case of this is just to make two triangles (a square) covering the whole circular area of the sector. But that’s wasteful, especially as our fragment shader grows more complex, which it’s about to do. Also, on a tiled renderer (which means almost every mobile GPU in practice), if we can reduce how many tiles the geometry intersects, we get some additional performance wins. So, we want to have enough triangles that our geometry approximates our real shape, but not so many that we’re making the scene-graph or vertex processing elements do unnecessary work.

All about the pixels (baby)

The challenge facing us is: we need to define what color the pixels of our geometry end up with, and that’s decided for each pixel by a shader program (written in GLSL for OpenGL, SPIR-V for Vulkan, and similar formats for other QtQuick backends). In the QtQuick scene graph system, shaders are interfaced to C++ via a material (QSGMaterial) subclass.

The main task of our material (and shader), then, is to answer two questions about a pixel:

  •  Is it outside our shape, part of the border, or somewhere inside?
  •  What colour should it be? (based on its position in the sector and the various colors defined in properties from QML)

The shader is a program within our program, executed by the GPU, which answers the above two questions for each and every pixel in our triangles, every time the scene is rendered (which is usually sixty times per second in Qt Quick). Fortunately, GPUs are fast and perform this work in parallel.

With the geometry, we had to define an array of X and Y values. But for the material side, we now need to get into the details of shading languages. We’re going to focus on OpenGL, since it’s the most common. But, fortunately, all shading languages are very similar. The OpenGL shading language is called GLSL. If you’ve ever experimented with the ShaderEffect item in QtQuick, you’ll have a good idea what is required, but there’s some book-keeping needed. Most importantly, we have to define our shader source code as some GLSL code. In my item, I do this as text strings inside the C++ source (you could also put them in a resource and load them via QFile). But, apart from the code, we also need to define the control values we will pass into our shader from C++. These values are called uniform values in OpenGL, and we need to manage them in a particular way for performance reasons.

The basic principle is to put all our control values into a structure, and ensure the scene-graph can compare two examples of our structure for equality. This means, if we have several sectors on the screen using the same colors and border, the scene-graph can identify that they use identical uniform values. Why is this important? Well, the most beneficial optimization strategy that the renderer performs is to group geometry that can be drawn at the same time into batches, to reduce the number of drawing calls and state changes. To allow multiple geometries to be batched, they must meet certain criteria. Having the material state be consistent is one of them. That’s why we follow some standard patterns, inherit from QSGSimpleMaterialShader, and use the macro QSG_DECLARE_SIMPLE_COMPARABLE_SHADER so that our shader can be compared with other instances and hopefully batched together when rendering.

struct SectorMaterialState
{
    QColor startInnerColor, startOuterColor,
        endInnerColor, endOuterColor, borderColor;
    float startAngleRad, spanAngleRad, borderWidth, innerRadius, outerRadius;

    int compareColor(const QColor& c1, const QColor& c2) const
    {
        const uint rgb = c1.rgba();
        const uint otherRgb = c2.rgba();
        return (rgb == otherRgb) ? 0 : ((rgb < otherRgb) ? -1: 1);
    }

    int compareFloat(const float& c1, const float& c2) const
    {
        return qFuzzyCompare(c1,c2) ? 0 : (c1 < c2) ? -1 : 1;
    }

    int compare(const SectorMaterialState *other) const
    {
        int r;
        if ((r = compareFloat(startAngleRad, other->startAngleRad)) != 0)
            return r;
        if ((r = compareFloat(spanAngleRad, other->spanAngleRad)) != 0)
            return r;
        if ((r = compareFloat(borderWidth, other->borderWidth)) != 0)
            return r;
        if ((r = compareFloat(innerRadius, other->innerRadius)) != 0)
            return r;
        if ((r = compareFloat(outerRadius, other->outerRadius)) != 0)
            return r;

        if ((r = compareColor(startInnerColor, other->startInnerColor)) != 0)
            return r;
        if ((r = compareColor(startOuterColor, other->startOuterColor)) != 0)
            return r;
        if ((r = compareColor(endInnerColor, other->endInnerColor)) != 0)
            return r;
        if ((r = compareColor(borderColor, other->borderColor)) != 0)
            return r;
        return compareColor(endOuterColor, other->endOuterColor);
    }
};

You could skip using the comparable stuff. But if you want to have ten or fifty sectors on screen at once, making them batch-able becomes significant. (Maybe you want to re-create the engine display panel for the ‘Classic’ series of 737s.)

The job of our material class, then, is to collect all the parameter data, be able to compare it in a robust way (often with some tolerances, for floating point values: qFuzzyCompare helps), and to map the parameters in C++ to uniforms on our shader. It also has to load the shader source from a file, resource to text string. Fortunately, the QSGMaterial subclasses have helpers to make all of this relatively easy, and handle some other complexities, as well (such as variations between desktop and embedded GLSL shaders).

Writing the fragment shader

To answer our ‘is it inside our shape?’ and ‘what colour should it be?’ questions for each pixel, we need to convert back into polar coordinates inside our pixel. We need to know our angular position to calculate the gradient color smoothly. And we need the radius to work out, if we’re inside or outside the shape, or on the border. That’s actually why we passed some additional data along with each vertex, to give us an easy numerical range to work with in our shader. We use a coordinate system where the center of the sector is always (0,0). To convert back to polar form, we use the atan2() function in GLSL, followed by a length() function to compute the radius. (For those of you familiar with OpenGL, we’re using the texture coordinates U and V to encode this simplified position alongside the real X and Y values. We could compute the values from X and Y in the shader with some additional uniforms. But with the limited number of triangles used, the overhead of two additional attributes per vertex is minimal.)

Then, we can check our radius against the inner, outer, and border values (passed in via uniforms), to see what we need to do. If we’re outside the outer radius, we can discard the pixel (analogous to an early-return in a C++ function). Discarding pixels can save some effort for the renderer, if done early in the fragment shader.

If we’re inside the border, we can use the border color. Otherwise, we need to compute an interior color. The built-in GLSL function mix() helps us to smoothly interpolate (blend) between two colors. To implement anti-aliasing, we also check if we’re near the transition between the interior and the border, or between the border and the outside. In those cases, we modify the pixel’s transparency, or mix the color with the border, to give a smooth transition.

Four sample fragments in our shader

  1. Outside the shape : discarded
  2. On the transition between the the border and the main fill – borderOpacity will be for example 0.5
  3. Within the fill: color in interpolated between the four inner colors to give final pixel RGB. In the code below, tNorm will be approximately 0.5 (half way between inner and outer radius)
  4. Fully within the border: borderOpacity is 1.0

Let’s express that in some GLSL:

uniform lowp float qt_Opacity;

uniform lowp vec4 startInnerColor;
uniform lowp vec4 startOuterColor;
uniform lowp vec4 endInnerColor;
uniform lowp vec4 endOuterColor;
uniform lowp float startAngleRad;
uniform lowp float spanAngleRad;
uniform lowp float innerRadius;
uniform lowp float outerRadius;
uniform lowp float borderWidth;
uniform lowp vec4 borderColor;
const lowp float aaWidth = 0.1;

varying lowp vec2 vpos;

void main()
{
            float theta = atan(vpos.y, vpos.x);
            float t = length(vpos);

            // are we between the inner and outer radius? With some blending at the edge based on AA width
            float baseOpacity = smoothstep(innerRadius - aaWidth, innerRadius + aaWidth, t) *
                (1.0 - smoothstep(outerRadius - aaWidth, outerRadius + aaWidth, t));

            // discard if we're outside the area
            if (baseOpacity &amp;lt; 0.05) { discard; }

            // border color handling : are within a borderWidth of either edge? Again compute an opacity
            // based on the distance and the AA width.
            float borderOpacity =
               (1.0 - smoothstep(innerRadius + borderWidth - aaWidth, innerRadius + borderWidth + aaWidth, t)) +
               smoothstep(outerRadius - borderWidth - aaWidth, outerRadius - borderWidth + aaWidth, t);

            // radial gradient: compute our t parameter which 0.0 at the inner radius and 1.0 at the outer
            float tNorm = (t - innerRadius) / (outerRadius - innerRadius);

            // gradient along the sector: compute S parameter which is 0.0 at startAngleRad, and 1.0
            // at the ending angle. Use mod() to handle wrapping 
            float s = mod(theta - startAngleRad, 6.28318) / spanAngleRad;

            // mix our four gradient colors together
            vec4 sc = mix(startInnerColor, startOuterColor, tNorm);
            vec4 ec = mix(endInnerColor, endOuterColor, tNorm);
            vec4 fillColor = mix(sc, ec, s);

            // finally mix in our border based on its opacity
            vec4 srcColor = mix(fillColor, borderColor, borderOpacity);
            gl_FragColor =  srcColor * baseOpacity * qt_Opacity;
}

Our GLSL fragment shader has main(), just like a regular C++ program, but it runs millions of times per second (in parallel, of course). We start by doing our atan call, and then we proceed to work out our color value based on that.

You may notice the code always applies a border and the possible gradient options. Why not use if-s to avoid this extra work? Well, shader programs really hate branching, whereas multiplying values by zero is optimized explicitly. Thus, it’s usually (but not always) advisable to write straight-line code without branching, if possible. This especially applies on lower powered embedded GPUs. Of course, if we had many sectors where a gradient was not required, we could create a different shader program without that feature and select one program or the other in our C++ code. This would keep the GLSL code friendly to our GPU, while allowing us to squeeze as much performance as possible out of the hardware. And our UX designers would never need to care.

Final result

Putting this all together, we define our QSGMaterial subclass, our state struct, and code, to look up the uniform values which pass information from C++ into GLSL. There are quite a few small points to get correct. But, in the end, we have a beautifully rendered sector on the screen, and an API we can give to our UX designers, confident that even if they go crazy with adding sectors, animating them, making them pulse, adding big and small ones, the performance should be excellent.

The good news is that, having done this once, doing it for other shapes is more of the same. On the geometry side, figure out some triangles which cover the pixels (fragments) of your shape. Inside your shader, use the vertex information, any uniforms you need, and possibly some other vertex information to compute if you’re within your shape, and what color to give it. It’s possible to do much more complex techniques, of course, creating procedural patterns following the many examples on ShaderToy, or defining vertices, dynamically, based on a real-time input (for example, an audio waveform).

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 Efficient custom shapes in QtQuick : shaders appeared first on KDAB.

How to generate barcode in Qt/QML application

Nowadays, everywhere we look, no matter if it’s a real-life shop or web page, we can see these little, simple and useful, black and white rectangles. Barcodes & QR codes are everywhere, with their usefulness proven over the years. That is why we decided to create a custom Qt & QML wrapper for barcode & QR code processing.

In this post, we will show how easy it is to generate and display a barcode and what’s going on behind the scenes in our code.

SCodes – barcode & QR code generation

So, to be able to scan or read a barcode (check out our previous blog post about scanning barcodes) first, you need to generate one. With the help of fantastic ZXing (Zebra Crossing) barcode image processing library (in this case we mean its port to C++) and Qt framework. We, at Scythe Studio, created an easy to use library SCodes which can generate and scan barcodes. It can be paired with QML to help us display our result in a simple way no matter the platform we are on.

Here is a little demo of our app, which you can check out at our GitHub:

QR_genereting_demo

 

ZXing C++ library usage in SCodes

ZXing-C++ library will help us with barcodes at a lower level. It supports reading and generating many formats of codes but here we will focus on 1-dimensional Code 128 and 2-dimensional QR Code and Data Matrix. The difference between them in our application is simply the format property because ZXing takes care of all string-to-image conversion.

The library provides a very simple API:

  1. ZXing::MultiFormatWriter class takes a barcode format as constructor’s argument and lets us set encoding, margin, and ECC level (error correction code level)

  2. We call an encode() method of the class mentioned above with our text content and output image size as arguments

  3. It returns BitMatrix where true == black pixel and false == white pixel of binary barcode image

After we get the bitmap we can process it to image however we like. That’s all there is to it.

The barcode generation

Let’s take a look at the code and how it’s done.

 First we initialize the MultiFormatWriter class with desired options.

int margin = 1;
int eccLevel = 1;
ZXing::BarcodeFormat format = ZXing::BarcodeFormat::CODE_128;
ZXing::MultiFormatWriter writer = ZXing::MultiFormatWriter(format).setMargin(margin).setEccLevel(eccLevel);

Past the first step. Now that we have set up a writer, it’s time to call an encode() method to give us a bitmap we can work with later.

ZXing::Matrix<uint8_t> bitmap;
bitmap = ZXing::ToMatrix<uint8_t>(writer.encode(ZXing::TextUtfEncoding::FromUtf8(“text”), 500, 500));

Our bitmap is ready, so let’s save it as a PNG image. For this, we will use a part of the stb library which is shipped with ZXing.

std::string filepath = “barcode.png”;
stbi_write_png(filepath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0);

And it’s done! We have our barcode image.

 

Code insights – Qt/QML

First, to be able to interact between C++ and QML we need a proper class. It must contain properties of the output image exposed as Q_PROPERTY and a whole class exposed to QML which is done with QML_ELEMENT (available since Qt 5.15.0).

class SBarcodeGenerator: public QQuickItem {

  Q_OBJECT

  Q_PROPERTY(int width MEMBER _width NOTIFY widthChanged)
  Q_PROPERTY(int height MEMBER _height NOTIFY heightChanged)
  Q_PROPERTY(int margin MEMBER _margin NOTIFY marginChanged)
  Q_PROPERTY(int eccLevel MEMBER _eccLevel NOTIFY eccLevelChanged)
  Q_PROPERTY(QString fileName MEMBER _fileName NOTIFY fileNameChanged)
  Q_PROPERTY(ZXing::BarcodeFormat format MEMBER _format)
  Q_PROPERTY(QString extension MEMBER _extension)
  Q_PROPERTY(QString filePath MEMBER _filePath)
  Q_PROPERTY(QString inputText MEMBER _inputText)

  #if(QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
  QML_ELEMENT
  #endif

  …

  public slots: 
    bool process(const QString & inputString);
  …

  signals: 
    void processFinished();

  ...

  private: 
  int _width = 500;
  int _height = 500;
  int _margin = 10;
  int _eccLevel = -1;
  ZXing::BarcodeFormat _format = ZXing::BarcodeFormat::QR_CODE;
  QString _extension = "png";
  QString _fileName = "barcode";
  QString _filePath = "";
  QString _inputText = "";
  ZXing::Matrix < uint8_t > _bitmap = ZXing::Matrix < uint8_t > ();
}

These are the code snippets of our wrapper for displaying purposes. If you want to check out the whole wrapper, head to SCodes repository.

Now for the process function, it’s almost the same code as in case of MultiFormatWriter initialization. Additionally, we also create path, filename and depending on the extension, we save our image. After everything’s done, we emit a processFinished() signal so QML can do its work.

bool SBarcodeGenerator::process(const QString & inputString) {
  if (inputString.isEmpty()) {
    return false;
  }
  else {
    ZXing::MultiFormatWriter writer = ZXing::MultiFormatWriter(_format).setMargin(_margin).setEccLevel(_eccLevel);
    _bitmap = ZXing::ToMatrix < uint8_t > (writer.encode(ZXing::TextUtfEncoding::FromUtf8(inputString.toStdString()), _width, _height));
    _filePath = QDir::tempPath() + "/" + _fileName + "." + _extension;

    if (_extension == "png") {
      stbi_write_png(_filePath.toStdString().c_str(), _bitmap.width(), _bitmap.height(), 1, _bitmap.data(), 0);
    }
    else if (_extension == "jpg" || _extension == "jpeg") {
      stbi_write_jpg(_filePath.toStdString().c_str(), _bitmap.width(), _bitmap.height(), 1, _bitmap.data(), 0);
    }

    emit processFinished();
    return true;
  }
}

Now in the QML part, we start by creating the SBarcodeGenerator object so we can interact with the C++ class. When we hit the button, the barcodeGenerator will start processing our string (in this case it’s a simple “text”). After it finishes a signal is sent and our overwritten default signal handler will change the Image’s container source to the generated image and display it.

ApplicationWindow {
id: root
visible: true  
width: 500  
height: 500

SBarcodeGenerator {
    id: barcodeGenerator
    onProcessFinished: {
        image.source = "file:///" + barcodeGenerator.filePath
    }
}

Button {
    id: generateButton
    onClicked: {
        image.source = ""
        barcodeGenerator.process(qStr(“test”))
    }
}

Image {
    id: image
    anchors: parent
    width: parent.width 
    height: image.width
}
}

It’s as simple as it gets. Really. Of course, these are snippets, and we skipped some trivial parts of the code, so if you want to see complete and more complex application check out our library SCodes with 2 demos for scanning and generating barcodes.

Summary

As you can see generating barcodes with ZXing is pretty straightforward as well as displaying your result in QML. If you have any questions regarding the topic or you want Scythe Studio to develop a solution with barcode support don’t hesitate to contact us!

PDF Report generator — Generate custom PDF reports using reportlab & pdfrw

If your job involves generating PDF reports, invoices, etc. you have probably thought about automating that with Python. Python has some great libraries for working with PDF files, allowing you to read and write PDFs from scripts. But you can also use these libraries as the basic of simple GUI tools, giving you an easy way to auto-fill or edit PDF reports on the desktop.

In this tutorial we'll be using two libraries to create a custom PDF report filler. The data will be collected using a Qt form: just edit the fields, press "Generate" to get the filled out form in the folder. The two libraries we'll be using here are --

  • reportlab which allows you to create PDFs using text and drawing primitives
  • pdfrw a library for reading and extracting pages from existing PDFs

While we could use reportlab to draw the entire PDF, it's easier to design a template using external tools and then simply overlay the dynamic content on this. We can use pdfrw to read our template PDF and then extract a page, onto which we can then draw using reportlab. That allows us to overlay custom information (from our app) directly onto an existing PDF template, which we save under a new name.

In this example we're entering the fields manually, but you can modify the application to read the data for the PDF from an external CSV file & generate multiple PDFs from it.

Template PDF

For testing I've created a custom TPS report template using Google Docs and downloaded the page as PDF. The page contains a number of fields which are to be filled. In this tutorial, we'll write a PyQt form which a user can fill in and then write that data out onto the PDF at the correct place.

TPS Report

The template is in A4 format. Save it in the same folder as your script.

If you have another template you'd prefer to use, feel free to use that. Just remember that you'll need to adjust the positions of the form fields when writing it.

Laying out the Form view

Qt includes a QFormLayout layout which simplifies the process of generating simple form layouts. It works similarly to a grid, but you can add rows of elements together and strings are converted automatically to QLabel objects. Our skeleton application, including the full layout matching the template form (more or less) is shown below.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox

class Window(QWidget):

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

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


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

python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox


class Window(QWidget):

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

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


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

python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox


class Window(QWidget):

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

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


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

python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox


class Window(QWidget):

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

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


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

When writing tools to replace/automate paper forms, it's usually a good idea to try and mimic the layout of the paper form so it's familiar.

The above will give us the following layout in a window when run. You can already type things into the fields, but pressing the button won't do anything yet -- we haven't written the code to generate the PDF or hooked it up to the button.

The form layout

Generating a PDF

For PDF generation using a base template, we'll be combining reportlab and PdfReader. The process is as follows --

  1. Read in the template.pdf file using PdfReader, and extract the first page only.
  2. Create a reportlab Canvas object
  3. Use pdfrw.toreportlab.makerl to generate a canvas object then add it to the Canvas with canvas.doForm()
  4. Draw out custom bits on the Canvas
  5. Save the PDF to file

The code is shown below, this doesn't require Qt, you can save to a file and run as-is. When run the resulting PDF will be saved as result.pdf in the same folder.

python
from reportlab.pdfgen.canvas import Canvas
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl

outfile = "result.pdf"

template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)

canvas = Canvas(outfile)

xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)

ystart = 443

# Prepared by
canvas.drawString(170, ystart, "My name here")

canvas.save()

Since the process of generating the PDF is doing IO, it may take some time (e.g. if we loading files off network drives). Because of this, it is better to handle this in a separate thread. We'll define this custom thread runner next.

Running the generation in a separate thread

Since each generation is an isolated job, it makes sense to use Qt's QRunner framework to handle the process -- this also makes it simple later to for example add customizable templates per job. We're using the same approach seen in the Multithreading tutorial where we use a subclass of QRunner to hold our custom run code, and implement runner-specific signals on a separate subclass of QObject.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)

python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)

python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)

We've defined two signals here:

  • file_saved_as which emits the filename of the saved PDF file (on success)
  • error which emits errors as a string for debugging

We need a QThreadPool to add run our custom runner on. We can add this onto our MainWindowin the __init__ block.

python
class Window(QWidget):

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

        self.threadpool = QThreadPool()

Now we have the generator QRunner defined, we just need to implement the generate method to create the runner, pass it the data from our form fields and the start the generation running.

python
def generate(self):
    self.generate_btn.setDisabled(True)
    data = {
        'name': self.name.text(),
        'program_type': self.program_type.text(),
        'product_code': self.product_code.text(),
        'customer': self.customer.text(),
        'vendor': self.vendor.text(),
        'n_errors': str(self.n_errors.value()),
        'comments': self.comments.toPlainText()
    }
    g = Generator(data)
    g.signals.file_saved_as.connect(self.generated)
    g.signals.error.connect(print)  # Print errors to console.
    self.threadpool.start(g)

def generated(self, outfile):
    pass28

In this code we first disable the generate_btn so the user can't press the button multiple times while the generation is taking place. We then construct a dictionary of data from our widgets, using the .text() method to get the text from QLineEdit widgets, .value() to get the value from the QSpinBox and .toPlainText() to get the plain text representation of the QTextEdit. We convert the numeric value to a string, since we are placing text.

To actually generate the PDF we create an instance of the Generator runner we just defined, passing in the dictionary of data. We connect the file_saved_as signal to our generated method (defined at the bottom, but not doing anything yet) and the error signal to the standard Python print function: this will automatically print any errors to the console.

Finally, we take our Generator instance and pass it to our threadpool's .start() method to queue it to run (it should start immediately). We can then hook this method up to our button in the __init__ of our main window e.g.

python
    self.generate_btn.pressed.connect(self.generate)

If you run the app now, pressing the button will trigger the generation of the PDF and the result will be saved as result.pdf in the same folder as you started the app. So far we've only placed a single block of text on the page, so let's complete the generator to write all our fields in the correct place.

Completing the generator

Next we need to finish the text placement on the template. The trick here is to work out what the per-line spacing is for your template (depends on the font size etc.) and then calculate positions relative to the first line. The y coordinates increase up the page (so 0,0 is the bottom left) so in our code before, we define the ystart for the top line and then subtract 28 for each line.

python

ystart = 443

# Prepared by
canvas.drawString(170, ystart, self.data['name'])

# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))

# Device/Program Type
canvas.drawString(230, ystart-28, self.data['program_type'])

# Product code
canvas.drawString(175, ystart-(2*28), self.data['product_code'])

# Customer
canvas.drawString(315, ystart-(2*28), self.data['customer'])

# Vendor
canvas.drawString(145, ystart-(3*28), self.data['vendor'])

ystart = 250

# Program Language
canvas.drawString(210, ystart, "Python")

canvas.drawString(430, ystart, self.data['n_errors'])

Wrapping

For most of our form fields we can just output the text as-is, since there are no line breaks. If the text entered is too long, then it will overflow -- but if we wanted we can limit this on the fields themselves by setting a max length in characters, e.g.

python
field.setMaxLength(25)

For the comments field, things are a little more tricky. The field can be much longer, and lines need to be wrapped over multiple lines in the template. The field also accepts line breaks (by pressing Enter) which cause problems when written out to the PDF.

Line breaks show up as black squares

As you can see in the above screenshot, the line breaks appear as black squares in the text. The good news is that just removing the line breaks will make it easier to wrap: we can just wrap each line to a specified number of characters.

Since the characters are variable width this isn't perfect, but it shouldn't matter. If we wrap for a line-full of the widest characters (W) any real line will fit.

Python comes with the textwrap library built in, which we can use to wrap our text, once we've stripped the newlines.

python
import textwrap
comments = comments.replace('\n', ' ')
lines = textwrap.wrap(comments, width=80)

But we need to account for the first line being shorter, which we can do by wrapping first to the shorter length, re-joining the remainder, and re-wrapping it, e.g.

python
import textwrap
comments = comments.replace('\n', ' ')
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])

lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4]  # max lines, not including the first.

The comment markers on the wrap lines (45 & 55) show the wrap length needed to fit a line of Ws into the space. This is the shortest possible line, but not realistic. The values used should work with most normal text.

To do this properly we should calculate the actual size of each length of text in the document font and use that to inform the wrapper.

Once we have the lines prepared, we can print them onto the PDF by iterating through the list and decrementing the y position for each time. The spacing between the lines in our template document is 28.

python
comments = self.data['comments'].replace('\n', ' ')
if comments:
    lines = textwrap.wrap(comments, width=65) # 45
    first_line = lines[0]
    remainder = ' '.join(lines[1:])

    lines = textwrap.wrap(remainder, 75) # 55
    lines = lines[:4]  # max lines, not including the first.

    canvas.drawString(155, 223, first_line)
    for n, l in enumerate(lines, 1):
        canvas.drawString(80, 223 - (n*28), l)

This gives the following result with some sample lorem ipsum text.

Lorem ipsum wrapped

Automatically showing the result

When the file is created our runner returns the filename of the created file in a signal (currently it is always the same). It would be nice to present the resulting PDF to the user automatically, so they can check if everything looks good. On Windows we can use os.startfile to open a file with the default launcher for that type -- in this case opening the PDF with the default PDF viewer.

Since this isn't available on other platforms, we catch the error and instead show a QMessageBox

python

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")

Complete code

The complete code for PyQt5, PySide2, PyQt6 or PySide6 is shown below.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


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

python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


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

python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data:The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


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

python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data:The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


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

Generating from a CSV file

In the above example you need to type the data to fill in manually. This is fine if you don't have a lot of PDFs to generate, but not so much fun if you have an entire CSV file worth of data to generate reports for. In the example below, rather than present a list of form fields to the user we just ask for a source CSV file from which PDFs can be generated -- each row in the file generates a separate PDF file using the data in the file.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    finished = pyqtSignal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


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

python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot


from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    finished = Signal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


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

python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot


from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    finished = pyqtSignal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


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

python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot


from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    finished = Signal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

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

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


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

You can run this app using the template.pdf and this example CSV file to generate a few TPS reports.

Things to notice --

  • We now generate multiple files, so it doesn't make much sense to open them when they're finished. Instead, we always show the "complete" message, and only once. The signal file_saved_as has been renamed to finished and we've removed the filename str since it's no longer used.
  • The QLineEdit to get the filename is disabled so it's not possible to edit directly: the only way to set a source CSV file is to select the file directly, ensuring it's there.
  • We auto-generate the output filenames, based on the import filename and the current row number. The filename is taken from the input CSV: with a CSV named tps.csv files will be named tps-1.pdf, tps-2.pdf etc. Files are written out to the folder the source CSV is in.
  • Since some rows/files might miss required fields, we use .get() on the row dictionary with a default empty string.

Possible improvements

If you feel like improving on this code, there are a few things you could try

  • Make the template and output file location configurable -- use a Qt file dialogs
  • Load the field positions from a file alongside the template (JSON) so you can use the same form with multiple templates
  • Make the fields configurable -- this gets quite tricky, but you particular types (str, datetime, int, etc.) can have specific widgets assigned to them

Share your ideas in the forum.

See the complete PyQt5 tutorial, from first steps to complete applications with Python & Qt5.

A 3D Block Building Game in QML

Qt Quick Rendering Engine

Here, at KDAB, we get to spend 10% of our time on learning what we don’t know or practicing and improving what we already know. Recently, I decided to use that time to learn more about the Qt Quick Rendering Engine. The best way to do so, I found, is to use it in a way it wasn’t intended to be used: for making simple 3D graphics — creating my own little 3D paintings, as one would in Minecraft, starting with a ground plane. I’d like to take this time to share with you how to play.

How to play

My mini voxel-editor should be using Isometric Perspective to display colorful cubes, which can be placed in a world. Therefore, I create a QML Rectangle that contains the transformations necessary for isometric perspective.

By combining three transforms, I create my ground plane:

ground plane

  • 0 — Starting with a normal rectangle
  • 1 — Rotating 45 degrees (around transform origin)
  • 2 — Downscaling of the original y-component
  • 3 — Flipping of the coordinate system towards the viewer

These actions can be achieved by combining three 4-by-4 matrices for each of the transformations, via multiplication. Note that, although QML comes with predefined Transform Items, I use raw matrices for each transform, to keep the code consistent.

//The ISO perspective transform:
transform: Matrix4x4 {
    id: isometric_perspective
    property real rotationZ: 45/360 * Math.PI * 2
    property matrix4x4 flipMatrix:
        Qt.matrix4x4(1,  0, 0, 0,
                     0, -1, 0, 0,
                     0,  0, 1, 0,
                     0,  0, 0, 1)
    property matrix4x4 rotationZMatrix:
        Qt.matrix4x4(Math.cos(rotationZ), -Math.sin(rotationZ), 0, 0,
                     Math.sin(rotationZ),  Math.cos(rotationZ), 0, 0,
                     0, 0, 1, 0,
                     0, 0, 0, 1)
    property real flatness: 0.5
    property matrix4x4 scaleYMatrix:
        Qt.matrix4x4(1,   0, 0, 0,
                     0, flatness, 0, 0,
                     0,   0, 1, 0,
                     0,   0, 0, 1)
    matrix: flipMatrix.times(scaleYMatrix).times(rotationZMatrix)
}

Because all transforms originate in the top-right of my window, I position my world-origin to the bottom-center of my window, to reveal the full world inside of it. Since QML’s scene graph is hierarchical, all of its child Items (RectanglesCircles, …) are inherently in the right perspective and appear as lying on the ground of my iso-world. Both the empty world and the world filled with RectangleCircle, and a star Image can be seen here:

ground plane

Next would be the cubes. I already have the top faces, since they would be simple squares in my iso-world (just like the red rectangle in the image above). To “elevate” them to the cubes height (32px), I apply an offset of 32 in x, as well as in y, as a transform. Going up in the iso-world means adding the same value to x and y, thus moving straight out of the corner pointing towards you. Sometimes, this additional axis combined out of x and y is called the w axis, for the isometric perspectives. There is a certain ambiguity, still, since the viewer does not know if a shape was moved on the ground plane (a), or elevated (b). Small tricks in lighting, such as adding a shadow, can help here:

scene graph

For the side faces, I also would need an upwards-effect to make them rise from the bottom to the top of my cubes. This, I achieve with a transform matrix, increasing x as well as y. For the right side, this happens in the first row of the transform matrix. Conversely, the left side has this double increase in its second row.

scene graph

For the colors, I decided to simply make the top Qt.lighter(cubeColor), the left normal cubeColor, and the right Qt.darker(cubeColor). So, overall, it looks like light is coming from top left.

I found out that all MouseAreas are transformed, together with the visible Item (as they behave as transformed Rectangles). So, just by filling a MouseArea to each of the sides, I could make them clickable, individually, also working with all the transforms. To reuse the MouseArea, I refactored out a universal FaceArea, which allows you to create a cube at an arbitrarily chosen vector offset.

So, to give an example, I instantiate it with the vector Qt.vector3D(0, 0, 1), for the top face to create the new cube on top, when clicking.

//FaceArea.qml
MouseArea{
    property var creationOffset: Qt.vector3d(001)
    anchors.fill: parent
    acceptedButtons: Qt.AllButtons
    onWheel: {isoCube.color = Qt.hsla(Math.random(),0.80.51)}
    onClicked: {
        if (mouse.button & Qt.LeftButton) {
            isoWorld.createCubeAt(isoCube.xpos + creationOffset.x,
                                              isoCube.ypos + creationOffset.y,
                                              isoCube.level + creationOffset.z);
        else {
            isoCube.destroy()
        }
    }
}

There are 3 possibilities of interaction:

  • Wheel randomly changes the color
  • Normal left click adds a new cube on that face, using QML’s Dynamic Item Creation.
  • Right click deletes the cube.

Bonus: To make the whole thing a bit more vivid, I made it wobble up and down. I also added an indicator where new cubes are placed on the ground plane.

Here is the final result in a video:

Tip: To make the Cubes stack well, Z-ordering has to be done right. There are 3 simple rules:

  1. If it’s above, it is in front.
  2. If it has a lower x or y, it is in front (because it’s more towards the viewer)
  3. But “above” is way more important.

So, I simply set the z to 1000 * stacklevel – x – y, and it worked well.

Summary

Although QML is intended to be used with 2D graphics, mainly 2.5D, including interaction is well possible through Item-specific transforms. The QML Scene Graph implicitly allows nested transforms and makes perspective drawings an easy task.

I had lots of fun while playing and learning through this small example. Maybe it’s useful to you, or maybe it just makes you as happy as it made me.

The code for this mini Minecraft can be found and played with at: https://github.com/chsterz/isoworld

 

KDAB offers unique Qt expert services. Develop unique Qt and QML applications with KDAB’s expertise. For more information on our services surrounding Qt and QML, click here.

 

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 A 3D Block Building Game in QML appeared first on KDAB.

KDDockWidgets 1.3.0 released!

kddockwidgets logoWe’ve released KDDockWidgets 1.3.0! KDDockWidgets is a framework for custom-tailored docking systems in Qt.

The main highlights for this release are PySide6 and experimental QtQuick support.

The QtQuick backend isn’t production ready, but you can already build with -DKDDockWidgets_QTQUICK=ON, run the examples under examples/qtquick/dockwidgets/ and start reporting issues.

Here’s the full list of changes:

  • Experimental QtQuick support (#49)
  • PySide6 support
  • Added static DockWidgetBase::byName() (#126)
  • The enum KDDockWidgets::AddingOption has been deprecated, use KDDockWidgets::InitialVisibilityOption instead
  • You can now pass a preferred initial size to MainWindow::addDockWidget() (#95)
  • Added DockWidgetBase::Option_DeleteOnClose
  • Added Config::Flag_CloseOnlyCurrentTab
  • Layout restorer now restores maximized/minimized state too (#81)
  • Fixed dock indicators sometimes not appearing on Windows (#103)
  • Fixed Flag_NativeTitleBar not working
  • Fixed drag offset when dragging too fast with mouse
  • Fixed bug where last tab index position wouldn’t be remembered in case user had manually reordered tabs (#154)
  • Fixed crash when hosting a QQuickWidget (#150)
  • Fixed CMake Visual Studio generator not working
  • Sidebar overlays now maintain their size when toggled (#155)
  • Added DockWidget::setFloatingGeometry() (#144)

Additionally, we’ve released version 1.2.1. It’s just a bug fix release over the 1.2 branch, which is now closed.

  • Support for resizing dock widgets when they are in overlay/popup mode (autohide/sidebar feature)
  • Fixed title bar close button enabled state not being restored with Layout saver (#137)
  • Installs a version header (kddockwidgets_version.h) that defines a version string and other useful versioning macros (#138)
  • DockWidgetBase::eventFilter() is protected instead of private (regression vs v1.1) (#148) It’s recommended that you rebuild your application when updating KDDW, as MSVC encodes private/protected in the name mangling.
  • Fixed WASM build on Windows (#163)
  • Fixed sidebar overlay not getting hidden when clicking on the main window docking area (#157)

 

You can find 1.3.0 here and 1.2.1 here.

Prebuilt packages for some popular Linux distributions are here: https://build.opensuse.org/project/repositories/isv:KDAB

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 KDDockWidgets 1.3.0 released! appeared first on KDAB.

AST Matchmaking made easy

The upcoming version of Clang 12 includes a new traversal mode which can be used for easier matching of AST nodes.

I presented this mode at EuroLLVM and ACCU 2019, but at the time I was calling it “ignoring invisible” mode. The primary aim is to make AST Matchers easier to write by requiring less “activation learning” of the newcomer to the AST Matcher API. I’m analogizing to “activation energy” here – this mode reduces the amount of learning of new concepts must be done before starting to use AST Matchers.

The new mode is a mouthful – IgnoreUnlessSpelledInSource – but it makes AST Matchers easier to use correctly and harder to use incorrectly. Some examples of the mode are available in the AST Matchers reference documentation.

In clang-query, the mode affects both matching and dumping of AST nodes and it is enabled with:

set traversal IgnoreUnlessSpelledInSource

while in the C++ API of AST Matchers, it is enabled by wrapping a matcher in:

traverse(TK_IgnoreUnlessSpelledInSource, ...)

The result is that matching of AST nodes corresponds closely to what is written syntactically in the source, rather than corresponding to the somewhat arbitrary structure implicit in the clang::RecursiveASTVisitor class.

Using this new mode makes it possible to “add features by removing code” in clang-tidy, making the checks more maintainable and making it possible to run checks in all language modes.

Clang does not use this new mode by default.

Implicit nodes in expressions

One of the issues identified is that the Clang AST contains many nodes which must exist in order to satisfy the requirements of the language. For example, a simple function relying on an implicit conversion might look like.

struct A {
    A(int);
    ~A();
};

A f()
{
    return 42;
}

In the new IgnoreUnlessSpelledInSource mode, this is represented as

ReturnStmt
`-IntegerLiteral '42'
and the integer literal can be matched with
returnStmt(hasReturnValue(integerLiteral().bind("returnVal")))

In the default mode, the AST might be (depending on C++ language dialect) represented by something like:

ReturnStmt
`-ExprWithCleanups
  `-CXXConstructExpr
    `-MaterializeTemporaryExpr
      `-ImplicitCastExpr
        `-CXXBindTemporaryExpr
          `-ImplicitCastExpr
            `-CXXConstructExpr
              `-IntegerLiteral '42'

To newcomers to the Clang AST, and to me, it is not obvious what all of the nodes there are for. I can reason that an instance of A must be constructed. However, there are two CXXConstructExprs in this AST and many other nodes, some of which are due to the presence of a user-provided destructor, others due to the temporary object. These kinds of extra nodes appear in most expressions, such as when processing arguments to a function call or constructor, declaring or assigning a variable, converting something to bool in an if condition etc.

There are already AST Matchers such as ignoringImplicit() which skip over some of the implicit nodes in AST Matchers. Still though, a complete matcher for the return value of this return statement looks something like

returnStmt(hasReturnValue(
    ignoringImplicit(
        ignoringElidableConstructorCall(
            ignoringImplicit(
                cxxConstructExpr(hasArgument(0,
                    ignoringImplicit(
                        integerLiteral().bind("returnVal")
                        )
                    ))
                )
            )
        )
    ))

Another mouthful.

There are several problems with this.

  • Typical clang-tidy checks which deal with expressions tend to require extensive use of such ignoring...() matchers. This makes the matcher expressions in such clang-tidy checks quite noisy
  • Different language dialects represent the same C++ code with different AST structures/extra nodes, necessitating testing and implementing the check in multiple language dialects
  • The requirement or possibility to use these intermediate matchers at all is not easily discoverable, nor are the required matchers to saitsfy all language modes easily discoverable
  • If an AST Matcher is written without explicitly ignoring implicit nodes, Clang produces lots of surprising results and incorrect transformations

Implicit declarations nodes

Aside from implicit expression nodes, Clang AST Matchers also match on implicit declaration nodes in the AST. That means that if we wish to make copy constructors in our codebase explicit we might use a matcher such as

cxxConstructorDecl(
    isCopyConstructor()
    ).bind("prepend_explicit")

This will work fine in the new IgnoreUnlessSpelledInSource mode.

However, in the default mode, if we have a struct with a compiler-provided copy constructor such as:

struct Copyable {
    OtherStruct m_o;
    Copyable();
};

we will match the compiler provided copy constructor. When our check inserts explicit at the copy constructor location it will result in:

struct explicit Copyable {
    OtherStruct m_o;
    Copyable();
};

Clearly this is an incorrect transformation despite the transformation code “looking” correct. This AST Matcher API is hard to use correctly and easy to use incorrectly. Because of this, the isImplicit() matcher is typically used in clang-tidy checks to attempt to exclude such transformations, making the matcher expression more complicated.

Implicit template instantiations

Another surpise in the behavior of AST Matchers is that template instantiations are matched by default. That means that if we wish to change class members of type int to type safe_int for example, we might write a matcher something like

fieldDecl(
    hasType(asString("int"))
    ).bind("use_safe_int")

This works fine for non-template code.

If we have a template like

template  
struct TemplStruct {
    TemplStruct() {}
    ~TemplStruct() {}

private:
    T m_t;
};

then clang internally creates an instantiation of the template with a substituted type for each template instantation in our translation unit.

The new IgnoreUnlessSpelledInSource mode ignores those internal instantiations and matches only on the template declaration (ie, with the T un-substituted).

However, in the default mode, our template will be transformed to use safe_int too:

template  
struct TemplStruct {
    TemplStruct() {}
    ~TemplStruct() {}

private:
    safe_int m_t;
};

This is clearly an incorrect transformation. Because of this, isTemplateInstantiation() and similar matchers are often used in clang-tidy to exclude AST matches which produce such transformations.

Matching metaphorical code

C++ has multiple features which are designed to be simple expressions which the compiler expands to something less-convenient to write. Range-based for loops are a good example as they are a metaphor for an explicit loop with calls to begin and end among other things. Lambdas are another good example as they are a metaphor for a callable object. C++20 adds several more, including rewriting use of operator!=(...) to use !operator==(...) and operator<(...) to use the spaceship operator.

[I admit that in writing this blog post I searched for a metaphor for “a device which aids understanding by replacing the thing it describes with something more familiar” before realizing the recursion. I haven’t heard these features described as metaphorical before though…]

All of these metaphorical replacements can be explored in the Clang AST or on CPP Insights.

Matching these internal representations is confusing and can cause incorrect transformations. None of these internal representations are matchable in the new IgnoreUnlessSpelledInSource mode.

In the default matching mode, the CallExprs for begin and end are matched, as are the CXXRecordDecl implicit in the lambda and hidden comparisons within rewritten binary operators such as spaceship (causing bugs in clang-tidy checks).

Easy Mode

This new mode of AST Matching is designed to be easier for users, especially newcomers to the Clang AST, to use and discover while offering protection from typical transformation traps. It will likely be used in my Qt-based Gui Quaplah, but it must be enabled explicitly in existing clang tools.

As usual, feedback is very welcome!

KDSingleApplication: a class for single-instance policy applications

Another day, another blog about some of KDAB’s utility classes. Now it’s the turn of KDSingleApplication, a class that helps implement applications that follow the single-instance policy.

What is a single-instance policy?

Single-instance policy refers to those applications that want to spawn a unique instance for a given user session. Even when launched multiple times, such applications will not spawn a number of separate instances. Instead, the existing instance will be notified, somehow, and some action will be taken by that instance. Typically, its window will be shown (if hidden) and raised to the foreground.

This mechanism works even when the application is launched indirectly, like from a file manager when opening a file type associated with the application. In this case, the new instance (launched by the file manager) will simply tell the existing instance to open the selected file, and then the new instance will immediately quit.

The problem is: on most operating systems we do normally get a new instance every time we launch an application! Therefore, some logic is needed to honor a single-instance policy. KDSingleApplication implements such logic.

Talk is cheap, show us the code

KDSingleApplication is a redesign of QtSingleApplication and other similar code that lives around in a few repositories.

The biggest advantage of KDSingleApplication over the old solutions is that it does not replace QCoreApplication/QGuiApplication/QApplication by inheriting from them. Such an inheritance is, generally speaking, a symptom of bad design. Unless we modify the solution’s own code, we cannot control which application class we’re using. Moreover, we cannot use the solution and another custom subclass of the application class. This is useful in some scenarios, because there can be only one application object!

KDSingleApplication instead complements your application object. You’re simply supposed to create an application object (of whatever kind is necessary) and then create a KDSingleApplication object:

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    KDSingleApplication kdsa;

After that, we will need to check whether our instance is the main (“primary”) instance, or a “secondary” one. The idea is that primary instances can receive arbitrary messages from the secondary instances and act on them in application-specific ways (open a file, show and raise the application’s main window, and so on).

The check looks like this:

    if (kdsa.isPrimaryInstance()) {
        /* primary: listen to messages */
        QObject::connect(&kdsa, &KDSingleApplication::messageReceived, 
                         /* handle messages from the secondary instances */);
    } else {
        /* secondary: send message to the primary */
        kdsa.sendMessage("some message to the primary");
        
        /* quit */
        return 0;
    }

The example shipped with KDSingleApplication illustrates the handling of messages in more detail.

You can download KDSingleApplication from its GitHub repository: https://github.com/KDAB/KDSingleApplication; it is distributed under the MIT license. Yes, for various practical reasons, KDSingleApplication lives in its own repository. Nonetheless, enjoy!

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 KDSingleApplication: a class for single-instance policy applications appeared first on KDAB.