How to Modernize Legacy SCADA Apps

Industry Insights Blog Series

 

 

Michele Rossi

Director, Industry

Amit Nainawat

Director, Solutions Engineering, EMEA

 

Would you trust a safety-critical industrial device that relied on outdated technology?

With too many Supervisory Control and Data Acquisition (SCADA) systems reliant on legacy frameworks like .NET or outdated MFC technology, today’s industrial automation systems are in urgent need of modernization. With factories operating 24/7, industries must modernize these systems without disrupting operations.

 

From Classroom to Code: Innovative Qt Apps by Future Developers

In this blog post, I want to share how we collaborated with the Cologne University of Applied Sciences (German: TH Köln) to support a new generation of developers exploring C++ and Qt. As a TH Köln alumnus who began his journey at Qt, I was honored by the opportunity to return and contribute. Alongside the course story, we’re highlighting the work of five student teams — sharing their ideas, the applications they built, and what they learned along the way.

Built to Last: Five Best Practices for Building Reliable HMI

Industry Insights Blog Series

 

 

Michele Rossi

Director, Industry

Qichen Huang

Senior Solutions Engineer, EMEA

Maurice Kalinowski

Director, Product Management, Qt Framework

 

Your HMI architecture needs to stay reliable as technology evolves and user demands change. Short-term efficiency matters, but true success means designing for long-term adaptability. Here’s how to build HMIs that handle today's workloads and scale effortlessly into the future.

 

Modern TableView in QML: What’s New in Qt 6.8 and Beyond

Modern TableView in QML: What’s New in Qt 6.8 and Beyond

Over the years, the capabilities of QtQuick's TableView have evolved dramatically-from early custom implementations to well supported feature in Qt 6.8 and newer. In this article we explore the progression of QtQuick/QML's TableView, outline the limitations of early versions, and highlight newer features such as custom selection modes, header synchronization, and lightweight editing delegates. Check it out.

Continue reading Modern TableView in QML: What’s New in Qt 6.8 and Beyond at basysKom GmbH.

Qt for MCUs 2.10.1 released

Qt for MCUs 2.10.1 has been released and is available for download.  This patch release provides bug fixes and other improvements while maintaining source compatibility with Qt for MCUs 2.10 (see Qt for MCUs 2.10 blog post). This release does not add any new functionality.

Tier-2 platforms are updated to 2.10, refer to the supported platforms page for more details about these platforms.

New platforms to announce !

A light-weight and small form factor version of the Infineon TRAVEO T2G CYT4DN  mcu evaluation kit named 6M-Lite is now supported as a Tech Preview platform.  With this platform we have also extended support for GNU-based ModusToolbox™ tool chain.  This will enable users to quickly start with the evaluation process and develop sleek GUI applications. In the next release of ModusToolbox™ , Qt for MCUs template application will be available as part of the IDE. Stay tuned !

Microchip SAM9x75 is now supported as a Tier-3 platform on Yocto Linux. The Curiosity LAN kit can be quickly setup with a Qt for MCUs application in no time. We will further extend this port for bare-metal platform. 

Build a Desktop Sticky Notes Application with PySide6 & SQLAlchemy — Create moveable desktop reminders with Python

Do you ever find yourself needing to take a quick note of some information but have nowhere to put it? Then this app is for you! This virtual sticky notes (or Post-it notes) app allows you to keep short text notes quickly from anywhere via the system tray. Create a new note, paste what you need in. It'll stay there until you delete it.

The application is written in PySide6 and the notes are implemented as decoration-less windows, that is windows without any controls. Notes can be dragged around the desktop and edited at will. Text in the notes and note positions are stored in a SQLite database, via SQLAlchemy, with note details and positions being restored on each session.

This is quite a complicated example, but we'll be walking through it slowly step by step. The full source code is available, with working examples at each stage of the development if you get stuck.

Setting Up the Working Environment

In this tutorial, we'll use the PySide6 library to build the note app's GUI. We'll assume that you have a basic understanding of PySide6 apps.

To learn the basics of PySide6, check out the complete PySide6 Tutorials or my book Create GUI Applications with Python & PySide6

To store the notes between sessions, we will use SQLAlchemy with a SQLite database (a file). Don't worry if you're not familiar with SQLAlchemy, we won't be going deep into that topic & have working examples you can copy.

With that in mind, let's create a virtual environment and install our requirements into it. To do this, you can run the following commands:

sh
$ mkdir notes/
$ cd notes
$ python -m venv venv
$ source venv/bin/activate
(venv)$ pip install pyside6 sqlalchemy
cmd
> mkdir notes/
> cd notes
> python -m venv venv
> venv\Scripts\activate.bat
(venv)> pip install pyside6 sqlalchemy
sh
$ mkdir notes/
$ cd notes
$ python -m venv venv
$ source venv/bin/activate
(venv)$ pip install pyside6 sqlalchemy

With these commands, you create a notes/ folder for storing your project. Inside that folder, you create a new virtual environment, activate it, and install PySide6 and SQLAlchemy from PyPi.

For platform-specific troublshooting, check the Working With Python Virtual Environments tutorial.

Building the Notes GUI

Let's start by building a simple notes UI where we can create, move and close notes on the desktop. We'll deal with persistance later.

The UI for our desktop sticky notes will be a bit strange since there is no central window, all the windows are independent yet look identical (aside from the contents). We also need the app to remain open in the background, using the system tray or toolbar, so we can show/hide the notes again without closing and re-opening the application each time.

We'll start by defining a single note, and then deal with these other issues later. Create a new file named notes.py and add the following outline application to it.

python
import sys

from PySide6.QtWidgets import QApplication, QTextEdit, QVBoxLayout, QWidget

app = QApplication(sys.argv)


class NoteWindow(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)


note = NoteWindow()
note.show()
app.exec()

In this code we first create a Qt QApplication instance. This needs to be done before creating our widgets. Next we define a simple custom window class NoteWindow by subclassing QWidget. We add a vertical layout to the window, and enter a single QTextEdit widget. We then create an instance of this window object as note and show it by calling .show(). This puts the window on the desktop. Finally, we start up our application by calling app.exec().

You can run this file like any other Pythons script.

sh
python notes.py

When the applicaton launches you'll see the following on your desktop.

Our editable "note" on the desktop Simple "notes" window on the desktop

If you click in the text editor in the middle, you can enter some text.

Technically this is a note, but we can do better.

Styling our notes

Our note doesn't look anything like a sticky note yet. Let's change that by applying some simple styles to it.

Firstly we can change the colors of the window, textarea and text. In Qt there are multiple ways to do this -- for example, we could override the system palette definition for the window. However, the simplest approach is to use QSS, which is Qt's version of CSS.

python
import sys

from PySide6.QtWidgets import QApplication, QTextEdit, QVBoxLayout, QWidget

app = QApplication(sys.argv)


class NoteWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setStyleSheet(
            "background: #FFFF99; color: #62622f; border: 0; font-size: 16pt;"
        )
        layout = QVBoxLayout()
        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)


note = NoteWindow()
note.show()
app.exec()

In the code above we have set a background color of hex #ffff99 for our note window, and set the text color to hex #62622f a sort of muddy brown. The border:0 removes the frame from the text edit, which otherwise would appear as a line on the bottom of the window. Finally, we set the font size to 16 points, to make the notes easier to read.

If you run the code now you'll see this, much more notely note.

The note with our QSS styles applied The note with the QSS styling applied

Remove Window Decorations

The last thing breaking the illusion of a sticky note on the desktop is the window decorations -- the titlebar and window controls. We can remove these using Qt window flags. We can also use a window flag to make the notes appear on top of other windows. Later we'll handle hiding and showing the notes via a tray application.

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QApplication,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

app = QApplication(sys.argv)


class NoteWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(
            self.windowFlags()
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
        )
        self.setStyleSheet(
            "background: #FFFF99; color: #62622f; border: 0; font-size: 16pt;"
        )
        layout = QVBoxLayout()

        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)


note = NoteWindow()
note.show()
app.exec()

To set window flags, we need to import the Qt flags from the QtCore namespace. Then you can set flags on the window using .setWindowFlags(). Note that since windows have flags already set, and we don't want to replace them all, we get the current flags with .windowFlags() and then add the additional flags to it using boolean OR |. We've added two flags here -- Qt.WindowType.FramelessWindowHint which removes the window decorations, and Qt.WindowType.WindowStaysOnTopHint which keeps the windows on top.

Run this and you'll see a window with the decorations removed.

Note with the window decorations removed Note with the window decorations removed

With the window decorations removed you no longer have access to the close button. But you can still close the window using Alt-F4 (Windows) or the application menu (macOS).

While you can close the window, it'd be nicer if there was a button to do it. We can add a custom button using QPushButton and hook this up to the window's .close() method to re-implement this.

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QPushButton,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

app = QApplication(sys.argv)


class NoteWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(
            self.windowFlags()
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
        )
        self.setStyleSheet(
            "background: #FFFF99; color: #62622f; border: 0; font-size: 16pt;"
        )
        layout = QVBoxLayout()
        # layout.setSpacing(0)

        buttons = QHBoxLayout()
        self.close_btn = QPushButton("×")
        self.close_btn.setStyleSheet(
            "font-weight: bold; font-size: 25px; width: 25px; height: 25px;"
        )
        self.close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        self.close_btn.clicked.connect(self.close)
        buttons.addStretch()  # Add stretch on left to push button right.
        buttons.addWidget(self.close_btn)
        layout.addLayout(buttons)

        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)


note = NoteWindow()
note.show()
app.exec()

Our close button is created using QPushButton with a unicode multiplication symbol (an x) as the label. We set a stylesheet on this button to size the label and button. Then we set a custom cursor on the button to make it clearer that this is a clickable thing that performs an action. Finally, we connect the .clicked signal of the button to the window's close method self.close. The button will close the window.

Later we'll use this button to delete notes.

To add the close button to the top right of the window, we create a horizontal layout with QHBoxLayout. We first add a stretch, then the push button. This has the effect of pushing the button to the right. Finally, we add our buttons layout to the main layout of the note, before the text edit. This puts it at the top of the window.

Run the code now and our note is complete!

The complete note UI with close button The complete note UI with close button

Movable notes

The note looks like a sticky note now, but we can't move it around and there is only one (unless we run the application multiple times concurrently). We'll fix both of those next, starting with the moveability of the notes.

This is fairly straightforward to achieve in PySide because Qt makes the raw mouse events available on all widgets. To implement moving, we can intercept these events and update the position of the window based on the distance the mouse has moved.

To implement this, add the following two methods to the bottom of the NoteWindow class.

python
class NoteWindow(QWidget):
    # ... existing code skipped

    def mousePressEvent(self, e):
        self.previous_pos = e.globalPosition()

    def mouseMoveEvent(self, e):
        delta = e.globalPosition() - self.previous_pos
        self.move(self.x() + delta.x(), self.y() + delta.y())
        self.previous_pos = e.globalPosition()

Clicking and dragging a window involves three actions: the mouse press, the mouse move and the mouse release. We have defined two methods here mousePressEvent and mouseMoveEvent. In mousePressEvent we receive the initial press of the mouse and store the position where the click occurred. This method is only called on the initial press of the mouse when starting to drag the window.

The mouseMoveEvent is called on every subsequent move while the mouse button remains pressed. On each move we take the new mouse position and subtract the previous position to get the delta -- that is, the change in mouse position from the initial press to the current event. Then we move the window by that amount, storing the new previous position after the move.

The effect of this is that ever time the mouseMoveEvent method is called, the window moves by the amount that the mouse has moved since the last call. The window moves -- or is dragged -- by the mouse.

Multiple notes

The note looks like a note, it is now moveable, but there is still only a single note -- not hugely useful! Let's fix that now.

Currently we're creating the NoteWindow when the application starts up, just before we call app.exec(). If we create new notes while the application is running it will need to happen in a function or method, which is triggered somehow. This introduces a new problem, since we need to have some way to store the NoteWindow objects so they aren't automatically deleted (and the window closed) when the function or method exits.

Python automatically deletes objects when they fall out of scope if there aren't any remaining references to them.

We can solve this by storing the NoteWindow objects somewhere. Usually we'd do this on our main window, but in this app there is no main window. There are a few options here, but in this case we're going to use a simple dictionary.

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QPushButton,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

app = QApplication(sys.argv)

# Store references to the NoteWindow objects in this, keyed by id.
active_notewindows = {}


class NoteWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(
            self.windowFlags()
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
        )
        self.setStyleSheet(
            "background: #FFFF99; color: #62622f; border: 0; font-size: 16pt;"
        )
        layout = QVBoxLayout()

        buttons = QHBoxLayout()
        self.close_btn = QPushButton("×")
        self.close_btn.setStyleSheet(
            "font-weight: bold; font-size: 25px; width: 25px; height: 25px;"
        )
        self.close_btn.clicked.connect(self.close)
        self.close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        buttons.addStretch()  # Add stretch on left to push button right.
        buttons.addWidget(self.close_btn)
        layout.addLayout(buttons)

        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)

        # Store a reference to this note in the
        active_notewindows[id(self)] = self

    def mousePressEvent(self, e):
        self.previous_pos = e.globalPosition()

    def mouseMoveEvent(self, e):
        delta = e.globalPosition() - self.previous_pos
        self.move(self.x() + delta.x(), self.y() + delta.y())
        self.previous_pos = e.globalPosition()


def create_notewindow():
    note = NoteWindow()
    note.show()


create_notewindow()
create_notewindow()
create_notewindow()
create_notewindow()
app.exec()

In this code we've added our active_notewindows dictionary. This holds references to our NoteWindow objects, keyed by id(). Note that this is Python's internal id for this object, so it is consistent and unique. We can use this same id to remove the note. We add each note to this dictionary at the bottom of it's __init__ method.

Next we've implemented a create_notewindow() function which creates an instance of NoteWindow and shows it, just as before. Nothing else is needed, since the note itself handles storing it's references on creation.

Finally, we've added multiple calls to create_notewindow() to create multiple notes.

Multiple notes on the desktop Multiple notes on the desktop

Adding Notes to the Tray

We can now create multiple notes programatically, but we want to be able to do this from the UI. We could implement this behavior on the notes themselves, but then it wouldn't work if al the notes had been closed or hidden. Instead, we'll create a tray application -- this will show in the system tray on Windows, or on the macOS toolbar. Users can use this to create new notes, and quit the application.

There's quite a lot to this, so we'll step through it in stages.

Update the code, adding the imports shown at the top, and the rest following the definition of create_notewindow.

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QPushButton,
    QSystemTrayIcon,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

# ... code hidden up to create_notewindow() definition

create_notewindow()

# Create system tray icon
icon = QIcon("sticky-note.png")

# Create the tray
tray = QSystemTrayIcon()
tray.setIcon(icon)
tray.setVisible(True)


def handle_tray_click(reason):
    # If the tray is left-clicked, create a new note.
    if (
        QSystemTrayIcon.ActivationReason(reason)
        == QSystemTrayIcon.ActivationReason.Trigger
    ):
        create_notewindow()


tray.activated.connect(handle_tray_click)

app.exec()

In this code we've first create an QIcon object passing in the filename of the icon to use. I'm using a sticky note icon from the Fugue icon set by designer Yusuke Kamiyamane. Feel free to use any icon you prefer.

We're using a relative path here. If you don't see the icon, make sure you're running the script from the same folder or provide the path.

The system tray icon is managed through a QSystemTrayIcon object. We set our icon on this, and set the tray icon to visible (so it is not automatically hidden by Windows).

QSystemTrayIcon has a signal activated which fires whenever the icon is activated in some way -- for example, being clicked with the left or right mouse button. We're only interested in a single left click for now -- we'll use the right click for our menu shortly. To handle the left click, we create a handler function which accepts reason (the reason for the activation) and then checks this against QSystemTrayIcon.ActivationReason.Trigger. This is the reason reported when a left click is used.

If the left mouse button has been clicked, we call create_notewindow() to create a new instance of a note.

If you run this example now, you'll see the sticky note in your tray and clicking on it will create a new note on the current desktop! You can create as many notes as you like, and once you close them all the application will close.

The sticky note icon in the tray The sticky note icon in the tray

This is happening because by default Qt will close an application once all it's windows have closed. This can be disabled, but we need to add another way to quit before we do it, otherwise our app will be unstoppable.

Adding a Menu

To allow the notes application to be closed from the tray, we need a menu. Sytem tray menus are normally accessible through right-clicking on the icon. To implement that we can set a QMenu as a context menu on the QSystemTrayIcon. The actions in menus in Qt are defined using QAction.

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QMenu,
    QPushButton,
    QSystemTrayIcon,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

# ... code hidden up to handle_tray_click

tray.activated.connect(handle_tray_click)


# Don't automatically close app when the last window is closed.
app.setQuitOnLastWindowClosed(False)

# Create the menu
menu = QMenu()
add_note_action = QAction("Add note")
add_note_action.triggered.connect(create_notewindow)
menu.addAction(add_note_action)

# Add a Quit option to the menu.
quit_action = QAction("Quit")
quit_action.triggered.connect(app.quit)
menu.addAction(quit_action)

# Add the menu to the tray
tray.setContextMenu(menu)


app.exec()

We create the menu using QMenu. Actions are created using QAction passing in the label as a string. This is the text that will be shown for the menu item. The .triggered signal fires when the action is clicked (in a menu, or toolbar) or activated through a keyboard shortcut. Here we've connected the add note action to our create_notewindow function. We've also added an action to quit the application. This is connected to the built-in .quit slot on our QApplication instance.

The menu is set on the tray using .setContextMenu(). In Qt context menus are automatically shown when the user right clicks on the tray.

Finally, we have also disabled the behavior of closing the application when the last window is closed using app.setQuitOnLastWindowClosed(False). Now, once you close all the windows, the application will remain running in the background. You can close it by going to the tray, right-clicking and selecting "Quit".

If you find this annoying while developing, just comment this line out again.

We've had a lot of changes so far, so here is the current complete code.

python
import sys

from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QPushButton,
    QSystemTrayIcon,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

app = QApplication(sys.argv)

# Store references to the NoteWindow objects in this, keyed by id.
active_notewindows = {}


class NoteWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowFlags(
            self.windowFlags()
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
        )
        self.setStyleSheet(
            "background: #FFFF99; color: #62622f; border: 0; font-size: 16pt;"
        )
        layout = QVBoxLayout()

        buttons = QHBoxLayout()
        self.close_btn = QPushButton("×")
        self.close_btn.setStyleSheet(
            "font-weight: bold; font-size: 25px; width: 25px; height: 25px;"
        )
        self.close_btn.clicked.connect(self.close)
        self.close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        buttons.addStretch()  # Add stretch on left to push button right.
        buttons.addWidget(self.close_btn)
        layout.addLayout(buttons)

        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)

        # Store a reference to this note in the active_notewindows
        active_notewindows[id(self)] = self

    def mousePressEvent(self, e):
        self.previous_pos = e.globalPosition()

    def mouseMoveEvent(self, e):
        delta = e.globalPosition() - self.previous_pos
        self.move(self.x() + delta.x(), self.y() + delta.y())
        self.previous_pos = e.globalPosition()


def create_notewindow():
    note = NoteWindow()
    note.show()


create_notewindow()

# Create the icon
icon = QIcon("sticky-note.png")

# Create the tray
tray = QSystemTrayIcon()
tray.setIcon(icon)
tray.setVisible(True)


def handle_tray_click(reason):
    # If the tray is left-clicked, create a new note.
    if (
        QSystemTrayIcon.ActivationReason(reason)
        == QSystemTrayIcon.ActivationReason.Trigger
    ):
        create_notewindow()


tray.activated.connect(handle_tray_click)

app.exec()

If you run this now you will be able to right click the note in the tray to show the menu.

The sticky note icon in the tray showing its context menu The sticky note icon in the tray showing its context menu

Test the Add note and Quit functionality to make sure they're working.

So, now we have our note UI implemented, the ability to create and remove notes and a persistent tray icon where we can also create notes & close the application. The last piece of the puzzle is persisting the notes between runs of the application -- if we leave a note on the desktop, we want it to still be there if we come back tomorrow. We'll implement that next.

Setting up the Notes database

To be able to store and load notes, we need an underlying data model. For this demo we're using SQLAlchemy as an interface to an SQLite database. This provides an Object-Relational Mapping (ORM) interface, which is a fancy way of saying we can interact with the database through Python objects.

We'll define our database in a separate file, to keep the UI file manageable. So start by creating a new file named database.py in your project folder.

In that file add the imports for SQLAlchemy, and instantiate the Base class for our models.

python
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

Base = declarative_base()

Next in the same database.py file, define our note database model. This inherits from the Base class we've just created, by calling declarative_base()

python
class Note(Base):
    __tablename__ = "note"
    id = Column(Integer, primary_key=True)
    text = Column(String(1000), nullable=False)
    x = Column(Integer, nullable=False, default=0)
    y = Column(Integer, nullable=False, default=0)

Each note object has 4 properties:

  • id the unique ID for the given note, used to delete from the database
  • text the text content of the note
  • x the x position on the screen
  • y the y position on the screen

Next we need to create the engine -- in this case, this is our SQLite file, which we're calling notes.db.We can then create the tables (if they don't already exist). Since our Note class registers itself with the Base we can do that by calling create_all on the Base class.

python
engine = create_engine("sqlite:///notes.db")

Base.metadata.create_all(engine)

Save the database.py file and run it

sh
python database.py

After it is complete, if you look in the folder you should see the notes.db. This file contains the table structure for the Note model we defined above.

Finally, we need a session to interact with the database from the UI. Since we only need a single session when the app is running, we can go ahead and create it in this file and then import it into the UI code.

Add the following to database.py

python
# Create a session to handle updates.
Session = sessionmaker(bind=engine)
session = Session()

The final complete code for our database interface is shown below

python
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

Base = declarative_base()


class Note(Base):
    __tablename__ = "note"
    id = Column(Integer, primary_key=True)
    text = Column(String(1000), nullable=False)
    x = Column(Integer, nullable=False, default=0)
    y = Column(Integer, nullable=False, default=0)


engine = create_engine("sqlite:///notes.db")

Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()

Now that our data model is defined, and our database created, we can go ahead and interface our Notes model into the UI. This will allow us to load notes at startup (to show existing notes), save notes when they are updated and delete notes when they are removed.

Integrating the Data Model into our UI

Our data model holds the text content and x & y positions of the notes. To keep the active notes and model in sync we need a few things.

  1. Each NoteWindow must have it's own associated instance of the Note object.
  2. New Note objects should be created when creating a new NoteWindow.
  3. The NoteWindow should sync it's initial state to a Note if provided.
  4. Moving & editing a NoteWindow should update the data in the Note.
  5. Changes to Note should be synced to the database.

We can tackle these one by one.

First let's setup our NoteWindow to accept, and store a reference to Note objects if provided, or create a new one if not.

python
import sys

from database import Note
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QMenu,
    QPushButton,
    QSystemTrayIcon,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

app = QApplication(sys.argv)

# Store references to the NoteWindow objects in this, keyed by id.
active_notewindows = {}


class NoteWindow(QWidget):
    def __init__(self, note=None):
        super().__init__()

        # ... add to the bottom of the __init__ method

        if note is None:
            self.note = Note()
        else:
            self.note = note


In this code we've imported the Note object from our database.py file. In the __init__ of our NoteWindow we've added an optional parameter to receive a Note object. If this is None (or nothing provided) a new Note will be created instead. The passed, or created note, is then stored on the NoteWindow so we can use it later.

This Note object is still not being loaded, updated, or persisted to the database. So let's implement that next. We add two methods, load() and save() to our NoteWindow to handle the loading and saving of data.

python
from database import Note, session

# ... skipped other imports, unchanged.

class NoteWindow(QWidget):
    def __init__(self, note=None):
        super().__init__()

        # ... modify the close_btn handler to use delete.
        self.close_btn.clicked.connect(self.delete)


        # ... rest of the code hidden.

        # If no note is provided, create one.
        if note is None:
            self.note = Note()
            self.save()
        else:
            self.note = note
            self.load()

    # ... add the following to the end of the class definition.

    def load(self):
        self.move(self.note.x, self.note.y)
        self.text.setText(self.note.text)

    def save(self):
        self.note.x = self.x()
        self.note.y = self.y()
        self.note.text = self.text.toPlainText()
        # Write the data to the database, adding the Note object to the
        # current session and committing the changes.
        session.add(self.note)
        session.commit()

    def delete(self):
        session.delete(self.note)
        session.commit()
        del active_notewindows[id(self)]
        self.close()

The load() method takes the x and y position from the Note object stored in self.note and updates the NoteWindow position and content to match. The save() method takes the NoteWindow position and content and sets that onto the Note object. It then adds the note to the current database session and commits the changes

Each commit starts a new session. Adding the Note to the session is indicating that we want it's changes persisted.

The delete() method handles deletion of the current note. This involves 3 things:

  1. passing the Note object to session.delete to remove it from the database,
  2. deleting the reference to our window from the active_notewindows (so the object will be tidied up)
  3. calling .close() to hide the window immediately.

Usually (2) will cause the object to be cleaned up, and that will close the window indirectly. But that may be delayed, which would mean sometimes the close button doesn't seem to work straight away. We call .close() to make it immediate.

We need to modify the close_btn.clicked signal to point to our delete method.

Next we've added a load() call to the __init__ when a Note object is passed. We also call .save() for newly created notes to persist them immediately, so our delete handler will work before editing.

Finally, we need to handle saving the note whenever it changes. We have two ways that the note can change -- when it's moved, or when it's edited. For the first we could do this on each mouse move, but it's a bit redundant. We only care where the note ends up while dragging -- that is, where it is when the mouse is released. We can get this through the mouseReleased method.

python
from database import Note, session

# ... skipped other imports, unchanged.

class NoteWindow(QWidget):

    # ... add the mouseReleaseEvent to the events on the NoteWindow.

    def mousePressEvent(self, e):
        self.previous_pos = e.globalPosition()

    def mouseMoveEvent(self, e):
        delta = e.globalPosition() - self.previous_pos
        self.move(self.x() + delta.x(), self.y() + delta.y())
        self.previous_pos = e.globalPosition()

    def mouseReleaseEvent(self, e):
        self.save()

    # ... the load and save methods are under here, unchanged.

That's all there is to it: when the mouse button is released, we save the current content and position by calling .save().

You might be wondering why we don't just save the position at this point? Usually it's better to implement a single load & save (persist/restore) handler that can be called for all situations. It avoids needing implementations for each case.

There have been a lot of partial code changes in this section, so here is the complete current code.

python
import sys

from database import Note, session
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QMenu,
    QPushButton,
    QSystemTrayIcon,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

app = QApplication(sys.argv)

# Store references to the NoteWindow objects in this, keyed by id.
active_notewindows = {}


class NoteWindow(QWidget):
    def __init__(self, note=None):
        super().__init__()

        self.setWindowFlags(
            self.windowFlags()
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
        )
        self.setStyleSheet(
            "background: #FFFF99; color: #62622f; border: 0; font-size: 16pt;"
        )
        layout = QVBoxLayout()

        buttons = QHBoxLayout()
        self.close_btn = QPushButton("×")
        self.close_btn.setStyleSheet(
            "font-weight: bold; font-size: 25px; width: 25px; height: 25px;"
        )
        self.close_btn.clicked.connect(self.delete)
        self.close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        buttons.addStretch()  # Add stretch on left to push button right.
        buttons.addWidget(self.close_btn)
        layout.addLayout(buttons)

        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)

        self.text.textChanged.connect(self.save)

        # Store a reference to this note in the active_notewindows
        active_notewindows[id(self)] = self

        # If no note is provided, create one.
        if note is None:
            self.note = Note()
            self.save()
        else:
            self.note = note
            self.load()

    def mousePressEvent(self, e):
        self.previous_pos = e.globalPosition()

    def mouseMoveEvent(self, e):
        delta = e.globalPosition() - self.previous_pos
        self.move(self.x() + delta.x(), self.y() + delta.y())
        self.previous_pos = e.globalPosition()

    def mouseReleaseEvent(self, e):
        self.save()

    def load(self):
        self.move(self.note.x, self.note.y)
        self.text.setText(self.note.text)

    def save(self):
        self.note.x = self.x()
        self.note.y = self.y()
        self.note.text = self.text.toPlainText()
        # Write the data to the database, adding the Note object to the
        # current session and committing the changes.
        session.add(self.note)
        session.commit()

    def delete(self):
        session.delete(self.note)
        session.commit()
        del active_notewindows[id(self)]
        self.close()


def create_notewindow():
    note = NoteWindow()
    note.show()


create_notewindow()

# Create the icon
icon = QIcon("sticky-note.png")

# Create the tray
tray = QSystemTrayIcon()
tray.setIcon(icon)
tray.setVisible(True)


def handle_tray_click(reason):
    # If the tray is left-clicked, create a new note.
    if (
        QSystemTrayIcon.ActivationReason(reason)
        == QSystemTrayIcon.ActivationReason.Trigger
    ):
        create_notewindow()


tray.activated.connect(handle_tray_click)


# Don't automatically close app when the last window is closed.
app.setQuitOnLastWindowClosed(False)

# Create the menu
menu = QMenu()
# Add the Add Note option to the menu.
add_note_action = QAction("Add note")
add_note_action.triggered.connect(create_notewindow)
menu.addAction(add_note_action)

# Add a Quit option to the menu.
quit_action = QAction("Quit")
quit_action.triggered.connect(app.quit)
menu.addAction(quit_action)
# Add the menu to the tray
tray.setContextMenu(menu)


app.exec()

If you run the application at this point it will be persisting data to the database as you edit it.

If you want to look at the contents of the SQLite database I can recommend DB Browser for SQLite. It's open source & free.

The note data persisted to the SQLite database The note data persisted to the SQLite database

Starting up

So our notes are being created, added to the database, updated and deleted. The last piece of the puzzle is restoring the previous state at start up.

We already have all the bits in place for this, we just need to handle the startup itself. To recreate the notes we can query the database to get a list of Note objects and then iterate through this, creating new NoteWindow instances (using our create_notewindow function).

python
def create_notewindow(note=None):
    note = NoteWindow(note)
    note.show()


existing_notes = session.query(Note).all()

if existing_notes:
    for note in existing_notes:
        create_notewindow(note)
else:
    create_notewindow()

First we've modified the create_notewindow function to accept an (optional) Note object which is passed through to the created NoteWindow.

Using the session we query session.query(Note).all() to get all the Note objects. If there any, we iterate them creating them. If not, we create a single note with no associated Note object (this will be created inside the NoteWindow).

That's it! The full final code is shown below:

python
import sys

from database import Note, session
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QMenu,
    QPushButton,
    QSystemTrayIcon,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)

app = QApplication(sys.argv)

# Store references to the NoteWindow objects in this, keyed by id.
active_notewindows = {}


class NoteWindow(QWidget):
    def __init__(self, note=None):
        super().__init__()

        self.setWindowFlags(
            self.windowFlags()
            | Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
        )
        self.setStyleSheet(
            "background: #FFFF99; color: #62622f; border: 0; font-size: 16pt;"
        )
        layout = QVBoxLayout()

        buttons = QHBoxLayout()
        self.close_btn = QPushButton("×")
        self.close_btn.setStyleSheet(
            "font-weight: bold; font-size: 25px; width: 25px; height: 25px;"
        )
        self.close_btn.clicked.connect(self.delete)
        self.close_btn.setCursor(Qt.CursorShape.PointingHandCursor)
        buttons.addStretch()  # Add stretch on left to push button right.
        buttons.addWidget(self.close_btn)
        layout.addLayout(buttons)

        self.text = QTextEdit()
        layout.addWidget(self.text)
        self.setLayout(layout)

        self.text.textChanged.connect(self.save)

        # Store a reference to this note in the active_notewindows
        active_notewindows[id(self)] = self

        # If no note is provided, create one.
        if note is None:
            self.note = Note()
            self.save()
        else:
            self.note = note
            self.load()

    def mousePressEvent(self, e):
        self.previous_pos = e.globalPosition()

    def mouseMoveEvent(self, e):
        delta = e.globalPosition() - self.previous_pos
        self.move(self.x() + delta.x(), self.y() + delta.y())
        self.previous_pos = e.globalPosition()

    def mouseReleaseEvent(self, e):
        self.save()

    def load(self):
        self.move(self.note.x, self.note.y)
        self.text.setText(self.note.text)

    def save(self):
        self.note.x = self.x()
        self.note.y = self.y()
        self.note.text = self.text.toPlainText()
        # Write the data to the database, adding the Note object to the
        # current session and committing the changes.
        session.add(self.note)
        session.commit()

    def delete(self):
        session.delete(self.note)
        session.commit()
        del active_notewindows[id(self)]
        self.close()


def create_notewindow(note=None):
    note = NoteWindow(note)
    note.show()


existing_notes = session.query(Note).all()

if existing_notes:
    for note in existing_notes:
        create_notewindow(note)
else:
    create_notewindow()

# Create the icon
icon = QIcon("sticky-note.png")

# Create the tray
tray = QSystemTrayIcon()
tray.setIcon(icon)
tray.setVisible(True)


def handle_tray_click(reason):
    # If the tray is left-clicked, create a new note.
    if (
        QSystemTrayIcon.ActivationReason(reason)
        == QSystemTrayIcon.ActivationReason.Trigger
    ):
        create_notewindow()


tray.activated.connect(handle_tray_click)


# Don't automatically close app when the last window is closed.
app.setQuitOnLastWindowClosed(False)

# Create the menu
menu = QMenu()
# Add the Add Note option to the menu.
add_note_action = QAction("Add note")
add_note_action.triggered.connect(create_notewindow)
menu.addAction(add_note_action)

# Add a Quit option to the menu.
quit_action = QAction("Quit")
quit_action.triggered.connect(app.quit)
menu.addAction(quit_action)
# Add the menu to the tray
tray.setContextMenu(menu)


app.exec()

If you run the app now, you can create new notes as before, but when you exit (using the Quit option from the tray) and restart, the previous notes will reappear. If you close the notes, they will be deleted. On startup, if there are no notes in the database an initial note will be created for you.

Conclusion

That's it! We have a fully functional desktop sticky note application, which you can use to keep simple bits of text until you need them again. We've learnt how to build an application up step by step from a basic outline window. We've added basic styles using QSS and used window flags to control the appearance of notes on the desktop. We've also seen how to create a system tray application, adding context menus and default behaviours (via a left mouse click). Finally, we've created a simple data model using SQLAlchemy and hooked that into our UI to persist the UI state between runs of the applications.

Try and extend this example further, for example:

  • Add multicolor notes, using a Python list of hex colors and random.choice to select a new color each time a note is created. Can you persist this in the database too?
  • Add an option in the tray to show/hide all notes. Remember we have all the NoteWindow objects in active_notewindows. You can show and hide windows in Qt using .show() and .hide().

Think about some additional features you'd like or expect to see in a desktop notes application and see if you can add them yourself!

Model/View Drag and Drop in Qt - Part 3

by David Faure (KDAB)

Model/View Drag and Drop in Qt - Part 3

In this third blog post of the Model/View Drag and Drop series (part 1 and part 2), the idea is to implement dropping onto items, rather than in between items. QListWidget and QTableWidget have out of the box support for replacing the value of existing items when doing that, but there aren't many use cases for that. What is much more common is to associate a custom semantic to such a drop. For instance, the examples detailed below show email folders and their contents, and dropping an email onto another folder will move (or copy) the email into that folder.

Blog_Drag&Drop_Qt_part3-treeview-step1

Step 1

Initial state, the email is in the inbox

Blog_Drag&Drop_Qt_part3-treeview-step2

Step 2

Dragging the email onto the Customers folder

Blog_Drag&Drop_Qt_part3-treeview-step3

Step 3

Dropping the email

Blog_Drag&Drop_Qt_part3-treeview-step4

Step 4

The email is now in the customers folder

With Model/View separation

Example code can be found here for flat models and here for tree models.

Setting up the view on the drag side

☑ Call view->setDragDropMode(QAbstractItemView::DragOnly)
unless of course the same view should also support drops. In our example, only emails can be dragged, and only folders allow drops, so the drag and drop sides are distinct.

☑ Call view->setDragDropOverwriteMode(...)
true if moving should clear cells, false if moving should remove rows.
Note that the default is true for QTableView and false for QListView and QTreeView. In our example, we want to remove emails that have been moved elsewhere, so false is correct.

☑ Call view->setDefaultDropAction(Qt::MoveAction) so that the drag defaults to a move and not a copy, adjust as needed

Setting up the model on the drag side

To implement dragging items out of a model, you need to implement the following -- this is very similar to the section of the same name in the previous blog post, obviously:

class EmailsModel : public QAbstractTableModel
{
    ~~~
    Qt::ItemFlags flags(const QModelIndex &index) const override
    {
        if (!index.isValid())
            return {};
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled;
    }

    // the default is "copy only", change it
    Qt::DropActions supportedDragActions() const override { return Qt::MoveAction | Qt::CopyAction; }

    QMimeData *mimeData(const QModelIndexList &indexes) const override;

    bool removeRows(int position, int rows, const QModelIndex &parent) override;

☑ Reimplement flags() to add Qt::ItemIsDragEnabled in the case of a valid index

☑ Reimplement supportedDragActions() to return Qt::MoveAction | Qt::CopyAction or whichever you want to support (the default is CopyAction only).

☑ Reimplement mimeData() to serialize the complete data for the dragged items. If the views are always in the same process, you can get away with serializing only node pointers (if you have that) and application PID (to refuse dropping onto another process). See the previous part of this blog series for more details.

☑ Reimplement removeRows(), it will be called after a successful drop with MoveAction. An example implementation looks like this:

bool EmailsModel::removeRows(int position, int rows, const QModelIndex &parent)
{
    beginRemoveRows(parent, position, position + rows - 1);
    for (int row = 0; row < rows; ++row) {
        m_emailFolder->emails.removeAt(position);
    }
    endRemoveRows();
    return true;
}

Setting up the view on the drop side

☑ Call view->setDragDropMode(QAbstractItemView::DropOnly) unless of course it supports dragging too. In our example, we can drop onto email folders but we cannot reorganize the folders, so DropOnly is correct.

Setting up the model on the drop side

To implement dropping items into a model's existing items, you need to do the following:

class FoldersModel : public QAbstractTableModel
{
    ~~~    
    Qt::ItemFlags flags(const QModelIndex &index) const override
    {
        CHECK_flags(index);
        if (!index.isValid())
            return {}; // do not allow dropping between items
        if (index.column() > 0)
            return Qt::ItemIsEnabled | Qt::ItemIsSelectable; // don't drop on other columns
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled;
    }

    // the default is "copy only", change it
    Qt::DropActions supportedDropActions() const override { return Qt::MoveAction | Qt::CopyAction; }
  
    QStringList mimeTypes() const override { return {QString::fromLatin1(s_emailsMimeType)}; }
  
    bool dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
};

☑ Reimplement flags()
For a valid index (and only in that case), add Qt::ItemIsDropEnabled. As you can see, you can also restrict drops to column 0, which can be more sensible when using QTreeView (the user should drop onto the folder name, not onto the folder size).

☑ Reimplement supportedDropActions() to return Qt::MoveAction | Qt::CopyAction or whichever you want to support (the default is CopyAction only).

☑ Reimplement mimeTypes() - the list should include the MIME type used by the drag model.

☑ Reimplement dropMimeData()
to deserialize the data and handle the drop.
This could mean calling setData() to replace item contents, or anything else that should happen on a drop: in the email example, this is where we copy or move the email into the destination folder. Once you're done, return true, so that the drag side then deletes the dragged rows by calling removeRows() on its model.

bool FoldersModel::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    ~~~  // safety checks, see full example code

    EmailFolder *destFolder = folderForIndex(parent);

    const QByteArray encodedData = mimeData->data(s_emailsMimeType);
    QDataStream stream(encodedData);
    ~~~ // code to detect and reject dropping onto the folder currently holding those emails

    while (!stream.atEnd()) {
        QString email;
        stream >> email;
        destFolder->emails.append(email);
    }
    emit dataChanged(parent, parent); // update count

    return true; // let the view handle deletion on the source side by calling removeRows there
}

Using item widgets

Example code:

On the "drag" side

☑ Call widget->setDragDropMode(QAbstractItemView::DragOnly) or DragDrop if it should support both

☑ Call widget->setDefaultDropAction(Qt::MoveAction) so that the drag defaults to a move and not a copy, adjust as needed

☑ Reimplement Widget::mimeData() to serialize the complete data for the dragged items. If the views are always in the same process, you can get away with serializing only item pointers and application PID (to refuse dropping onto another process). In our email folders example we also serialize the pointer to the source folder (where the emails come from) so that we can detect dropping onto the same folder (which should do nothing).

To serialize pointers in QDataStream, cast them to quintptr, see the example code for details.

On the "drop" side

☑ Call widget->setDragDropMode(QAbstractItemView::DropOnly) or DragDrop if it should support both

☑ Call widget->setDragDropOverwriteMode(true) for a minor improvement: no forbidden cursor when moving the drag between folders. Instead Qt only computes drop positions which are onto items, as we want here.

☑ Reimplement Widget::mimeTypes() and return the same name as the one used on the drag side's mimeData

☑ Reimplement Widget::dropMimeData() (note that the signature is different between QListWidget, QTableWidget and QTreeWidget) This is where you deserialize the data and handle the drop. In the email example, this is where we copy or move the email into the destination folder.

Make sure to do all of the following:

  • any necessary behind the scenes work (in our case, moving the actual email)
  • updating the UI (creating or deleting items as needed)

This is a case where proper model/view separation is actually much simpler.

Improvements to Qt

While writing and testing these code examples, I improved the following things in Qt, in addition to those listed in the previous blog posts:

  • QTBUG-2553 QTreeView with setAutoExpandDelay() collapses items while dragging over it, fixed in Qt 6.8.1

Conclusion

I hope you enjoyed this blog post series and learned a few things.

The post Model/View Drag and Drop in Qt - Part 3 appeared first on KDAB.

Model/View Drag and Drop in Qt - Part 2

by David Faure (KDAB)

Model/View Drag and Drop in Qt - Part 2

In the previous blog, you learned all about moving items within a single view, to reorder them.

In part 2, we are still talking about moving items, and still about inserting them between existing items (never overwriting items) but this time the user can move items from one view to another. A typical use case is a list of available items on the left, and a list of selected items on the right (one concrete example would be to let the user customize which buttons should appear in a toolbar). This also often includes reordering items in the right-side list, the good news being that this comes for free (no extra code needed).

Blog_Drag&Drop_Qt_part2-step1

Moving a row between treeviews, step 1

Blog_Drag&Drop_Qt_part2-step2

Moving a row between treeviews, step 2

Blog_Drag&Drop_Qt_part2-step3

Moving a row between treeviews, step 3

With Model/View separation

Example code for flat models and example code for tree models.

Setting up the view on the drag side

To allow dragging items out of the view, make sure to do the following:

☑ Call view->setDragDropMode(QAbstractItemView::DragOnly) (or DragDrop if it should support both).

☑ Call view->setDragDropOverwriteMode(false) so that QTableView calls removeRows when moving rows, rather than just clearing their cells

☑ Call view->setDefaultDropAction(Qt::MoveAction) so it's a move and not a copy

Setting up the model on the drag side

To implement dragging items out of a model, you need to implement the following:

class CountryModel : public QAbstractTableModel
{
    ~~~
    Qt::ItemFlags flags(const QModelIndex &index) const override
    {
        if (!index.isValid())
            return {}; // depending on whether you want drops as well (next section)
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled;
    }

    // the default is "return supportedDropActions()", let's be explicit
    Qt::DropActions supportedDragActions() const override { return Qt::MoveAction; }

    QMimeData *mimeData(const QModelIndexList &indexes) const override; // see below

    bool removeRows(int position, int rows, const QModelIndex &parent) override; // see below
};

More precisely, the check-list is the following:

☑ Reimplement flags() to add Qt::ItemIsDragEnabled in the case of a valid index

☑ Reimplement supportedDragActions() to return Qt::MoveAction

☑ Reimplement mimeData() to serialize the complete data for the dragged items. If the views are always in the same process, you can get away with serializing only node pointers (if you have that, e.g. for tree models) and application PID (to refuse dropping onto another process). Otherwise you can encode the actual data, like this:

QMimeData *CountryModel::mimeData(const QModelIndexList &indexes) const
{
    QByteArray encodedData;
    QDataStream stream(&encodedData, QIODevice::WriteOnly);
    for (const QModelIndex &index : indexes) {
        // This calls operator<<(QDataStream &stream, const CountryData &countryData), which you must implement
        stream << m_data.at(index.row());
    }

    QMimeData *mimeData = new QMimeData;
    mimeData->setData(s_mimeType, encodedData);
    return mimeData;
}

s_mimeType is the name of the type of data (make up a name, it usually starts with application/x-)

☑ Reimplement removeRows(), it will be called after a successful drop. For instance, if your data is in a vector called m_data, the implementation would look like this:

bool CountryModel::removeRows(int position, int rows, const QModelIndex &parent)
{
    beginRemoveRows(parent, position, position + rows - 1);
    for (int row = 0; row < rows; ++row)
        m_data.removeAt(position);
    endRemoveRows();
    return true;
}

Setting up the view on the drop side

☑ Call view->setDragDropMode(QAbstractItemView::DragDrop) (already done if both views should support dragging and dropping)

Setting up the model on the drop side

To implement dropping items into a model (between existing items), you need to implement the following:

class DropModel : public QAbstractTableModel
{
    ~~~
    Qt::ItemFlags flags(const QModelIndex &index) const override
    {
        if (!index.isValid())
            return Qt::ItemIsDropEnabled;
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable; // and optionally Qt::ItemIsDragEnabled (previous section)
    }

    // the default is "copy only", change it
    Qt::DropActions supportedDropActions() const override { return Qt::MoveAction; }

    QStringList mimeTypes() const override { return {QString::fromLatin1(s_mimeType)}; }

    bool dropMimeData(const QMimeData *mimeData, Qt::DropAction action, 
                      int row, int column, const QModelIndex &parent) override; // see below
};

☑ Reimplement supportedDropActions() to return Qt::MoveAction

☑ Reimplement flags()
For a valid index, make sure Qt::ItemIsDropEnabled is NOT set (except for tree models where we need to drop onto items in order to insert a first child).
For the invalid index, add Qt::ItemIsDropEnabled, to allow dropping between items.

☑ Reimplement mimeTypes() and return the name of the MIME type used by the mimeData() function on the drag side.

☑ Reimplement dropMimeData()
to deserialize the data and insert new rows.
In the special case of in-process tree models, clone the dragged nodes.
In both cases, once you're done, return true, so that the drag side then deletes the dragged rows by calling removeRows() on its model.

bool DropModel::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    ~~~  // safety checks, see full example code

    if (row == -1) // drop into empty area = append
        row = rowCount(parent);

    // decode data
    const QByteArray encodedData = mimeData->data(s_mimeType);
    QDataStream stream(encodedData);
    QVector<CountryData> newCountries;
    while (!stream.atEnd()) {
        CountryData countryData;
        stream >> countryData;
        newCountries.append(countryData);
    }

    // insert new countries
    beginInsertRows(parent, row, row + newCountries.count() - 1);
    for (const CountryData &countryData : newCountries)
        m_data.insert(row++, countryData);
    endInsertRows();

    return true; // let the view handle deletion on the source side by calling removeRows there
}

Using item widgets

Example code can be found following this link.

For all kinds of widgets

On the "drag" side:

☑ Call widget->setDragDropMode(QAbstractItemView::DragOnly) or DragDrop if it should support both

☑ Call widget->setDefaultDropAction(Qt::MoveAction) so the drag starts as a move right away

On the "drop" side:

☑ Call widget->setDragDropMode(QAbstractItemView::DropOnly) or DragDrop if it should support both

☑ Reimplement supportedDropActions() to return only Qt::MoveAction

Additional requirements for QTableWidget

When using QTableWidget, in addition to the common steps above you need to:

On the "drag" side:

☑ Call item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled); for each item, to disable dropping onto items.

☑ Call widget->setDragDropOverwriteMode(false) so that after a move the rows are removed rather than cleared

On the "drop" side:

☑ Call widget->setDragDropOverwriteMode(false) so that it inserts rows instead of replacing cells (the default is false for the other views anyway)

☑ Another problem is that the items created by a drop will automatically get the Qt::ItemIsDropEnabled flag, which you don't want. To solve this, use widget->setItemPrototype() with an item that has the right flags (see the example).

Additional requirements for QTreeWidget

When using QTreeWidget, you cannot disable dropping onto items (which creates a child of the item).

You could call item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled); on your own items, but when QTreeWidget creates new items upon a drop, you cannot prevent them from having the flag Qt::ItemIsDropEnabled set. The prototype solution used above for QTableWidget doesn't exist for QTreeWidget.

This means, if you want to let the user build and reorganize an actual tree, you can use QTreeWidget. But if you just want a flat multi-column list, then you should use QTreeView (see previous section on model/view separation).

Addendum: Move/copy items between views

If the user should be able to choose between copying and moving items, follow the previous section and make the following changes.

With Model/View separation

On the "drag" side:

☑ Call view->setDefaultDropAction(...) to choose whether the default should be move or copy. The user can press Shift to force a move, and Ctrl to force a copy.

☑ Reimplement supportedDragActions() in the model to return Qt::MoveAction | Qt::CopyAction

On the "drop" side:

☑ Reimplement supportedDropActions() in the model to return Qt::MoveAction | Qt::CopyAction

The good news is that there's nothing else to do.

Using item widgets

On the "drag" side:

☑ Call widget->setDefaultDropAction(...) to choose whether the default should be move or copy. The user can press Shift to force a move, and Ctrl to force a copy.

Until Qt 6.10 there was no setSupportedDragActions() method in the item widget classes (that was QTBUG-87465, I implemented it for 6.10). Fortunately the default behavior is to use what supportedDropActions() returns so if you just want move and copy in both, reimplementing supportedDropActions() is enough.

On the "drop" side:

☑ Reimplement supportedDropActions() in the item widget class to return Qt::MoveAction | Qt::CopyAction

The good news is that there's nothing else to do.

Improvements to Qt

While writing and testing these code examples, I improved the following things in Qt:

  • QTBUG-1387 "Drag and drop multiple columns with item views. Dragging a row and dropping it in a column > 0 creates multiple rows.", fixed in 6.8.1
  • QTBUG-36831 "Drop indicator painted as single pixel when not shown" fixed in 6.8.1
  • QTBUG-87465 ItemWidgets: add supportedDragActions()/setSupportedDragActions(), implemented in 6.10

Conclusion

In the next blog post of this series, you will learn how to move (or copy) onto existing items, rather than between them.

The post Model/View Drag and Drop in Qt - Part 2 appeared first on KDAB.

Model/View Drag and Drop in Qt - Part 1

by David Faure (KDAB)

Model/View Drag and Drop in Qt - Part 1

This blog series is all about implementing drag-and-drop in the Qt model/view framework. In addition to complete code examples, you'll find checklists that you can go through to make sure that you did not forget anything in your own implementation, when something isn't working as expected.

At first, we are going to look at Drag and Drop within a single view, to change the order of the items. The view can be a list, a table or a tree, there are very little differences in what you have to do.

part1-table-step1

Moving a row in a tableview, step 1

part1-table-step2

Moving a row in a tableview, step 2

part1-table-step3

Moving a row in a tableview, step 3

The main question, however, is whether you are using QListView/QTableView/QTreeView on top of a custom item model, or QListWidget/QTableWidget/QTreeWidget with items in them. Let's explore each one in turn.

With Model/View separation

The code being discussed here is extracted from the example. That example features a flat model, while this example features a tree model. The checklist is the same for these two cases.

Setting up the view

☑ Call view->setDragDropMode(QAbstractItemView::InternalMove) to enable the mode where only moving within the same view is allowed

☑ When using QTableView, call view->setDragDropOverwriteMode(false) so that it inserts rows instead of replacing cells (the default is false for the other views anyway)

Adding drag-n-drop support to the model

part1-list

Reorderable ListView

part1-table

Reorderable TableView

For a model being used in QListView or QTableView, all you need is something like this:

class CountryModel : public QAbstractTableModel
{
    ~~~
    Qt::ItemFlags flags(const QModelIndex &index) const override
    {
        if (!index.isValid())
            return Qt::ItemIsDropEnabled; // allow dropping between items
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled;
    }

    // the default is "copy only", change it
    Qt::DropActions supportedDropActions() const override { return Qt::MoveAction; }

    // the default is "return supportedDropActions()", let's be explicit
    Qt::DropActions supportedDragActions() const override { return Qt::MoveAction; }

    QStringList mimeTypes() const override { return {QString::fromLatin1(s_mimeType)}; }

    bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) override; // see below
};

The checklist for the changes you need to make in your model is therefore the following:

☑ Reimplement flags()
For a valid index, add Qt::ItemIsDragEnabled and make sure Qt::ItemIsDropEnabled is NOT set (except for tree models where we need to drop onto items in order to insert a first child). \

☑ Reimplement mimeTypes() and make up a name for the mimetype (usually starting with application/x-)

☑ Reimplement supportedDragActions() to return Qt::MoveAction

☑ Reimplement supportedDropActions() to return Qt::MoveAction

☑ Reimplement moveRows()

Note that this approach is only valid when using QListView or, assuming Qt >= 6.8.0, QTableView - see the following sections for details.

In a model that encapsulates a QVector called m_data, the implementation of moveRows can look like this:

bool CountryModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
{
    if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild))
        return false; // invalid move, e.g. no-op (move row 2 to row 2 or to row 3)

    for (int i = 0; i < count; ++i) {
        m_data.move(sourceRow + i, destinationChild + (sourceRow > destinationChild ? 0 : -1));
    }

    endMoveRows();
    return true;
}

QTreeView does not call moveRows

part1-tree

Reorderable treeview

part1-treemodel

Reorderable treeview with a tree model

QTreeView does not (yet?) call moveRows in the model, so you need to:

☑ Reimplement mimeData() to encode row numbers for flat models, and node pointers for tree models

☑ Reimplement dropMimeData() to implement the move and return false (meaning: all done)

Note that this means a move is in fact an insertion and a deletion, so the selection isn't automatically updated to point to the moved row(s).

QTableView in Qt < 6.8.0

I implemented moving of rows in QTableView itself for Qt 6.8.0, so that moving rows in a table view is simpler to implement (one method instead of two), more efficient, and so that selection is updated. If you're not yet using Qt >= 6.8.0 then you'll have to reimplement mimeData() and dropMimeData() in your model, as per the previous section.

This concludes the section on how to implement a reorderable view using a separate model class.

Using item widgets

The alternative to model/view separation is the use of the item widgets (QListWidget, QTableWidget or QTreeWidget) which you populate directly by creating items.

part1-listwidget

Reorderable QListWidget

part1-tablewidget

Reorderable QTableWidget

part1-treewidget

Reorderable QTreeWidget

Here's what you need to do to allow users to reorder those items.

Example code can be found following this link.

Reorderable QListWidget

☑ Call listWidget->setDragDropMode(QAbstractItemView::InternalMove) to enable the mode where only moving within the same view is allowed

For a QListWidget, this is all you need. That was easy!

Reorderable QTableWidget

When using QTableWidget:

☑ Call tableWidget->setDragDropMode(QAbstractItemView::InternalMove)

☑ Call tableWidget->setDragDropOverwriteMode(false) so that it inserts rows instead of replacing cells

☑ Call item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled); on each item, to disable dropping onto items

Note: Before Qt 6.8.0, QTableWidget did not really support moving rows. It would instead move data into cells (like Excel). The example code shows a workaround, but since it calls code that inserts a row and deletes the old one, header data is lost in the process. My changes in Qt 6.8.0 implement support for moving rows in QTableWidget's internal model, so it's all fixed there. If you really need this feature in older versions of Qt, consider switching to QTableView.

Reorderable QTreeWidget

When using QTreeWidget:

☑ Call tableWidget->setDragDropMode(QAbstractItemView::InternalMove)

☑ Call item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled); on each item, to disable dropping onto items

Conclusion about reorderable item widgets

Of course, you'll also need to iterate over the items at the end to grab the new order, like the example code does. As usual, item widgets lead to less code to write, but the runtime performance is worse than when using model/view separation. So, only use item widgets when the number of items is small (and you don't need proxy models).

Improvements to Qt

While writing and testing these code examples, I improved the following things in Qt 6.8:

  • QTBUG-13873 / QTBUG-101475 - QTableView: implement moving rows by drag-n-drop
  • QTBUG-69807 - Implement QTableModel::moveRows
  • QTBUG-130045 - QTableView: fix dropping between items when precisely on the cell border
  • QTBUG-1656 - Implement full-row drop indicator when the selection behavior is SelectRows

Conclusion

I hope this checklist will be useful when you have to implement your own reordering of items in a model or an item-widget. Please post a comment if anything appears to be incorrect or missing.

In the next blog post of this series, you will learn how to move (or even copy) items from one view to another.

The post Model/View Drag and Drop in Qt - Part 1 appeared first on KDAB.

Improvements to Mozilla's Searchfox Code Browser

by Nicolas Guichard (KDAB)

Improvements to Mozilla's Searchfox Code Browser

Mozilla is the maker of the famous Firefox web browser and the birthplace of the likes of Rust and Servo (read more about Embedding the Servo Web Engine in Qt).

Firefox is a huge, multi-platform, multi-language project with 21 million lines of code back in 2020, according to their own blog post. Navigating in projects like those is always a challenge, especially at the cross-language boundaries and in platform-specific code.

To improve working with the Firefox code-base, Mozilla hosts an online code browser tailored for Firefox called Searchfox. Searchfox analyzes C++, JavaScript, various IDLs (interface definition languages), and Rust source code and makes them all browsable from a single interface with full-text search, semantic search, code navigation, test coverage report, and git blame support. It's the combination of a number of projects working together, both internal to Mozilla (like their Clang plugin for C++ analysis) and external (such as rust-analyzer maintained by Ferrous Systems).

It takes a whole repository in and separately indexes C++, Rust, JavaScript and now Java and Kotlin source code. All those analyses are then merged together across platforms, before running a cross-reference step and building the final index used by the web front-end available at searchfox.org.

EverythingInOne_Mozilla_ShowGraph_V3.png

Mozilla asked KDAB to help them with adding Java and Kotlin support to Searchfox in prevision of the merge of Firefox for Android into the main mozilla-central repository and enhance their C++ support with macro expansions. Let's dive into the details of those tasks.

Java/Kotlin Support

Mozilla merged the Firefox for Android source code into the main mozilla-central repository that Searchfox indexes. To add support for that new Java and Kotlin code to Searchfox, we reused open-source tooling built by Sourcegraph around the SemanticDB and SCIP code indexing formats. (Many thanks to them!)

Sourcegraph's semanticdb-javac and semanticdb-kotlinc compiler plugins are integrated into Firefox's CI system to export SemanticDB artifacts. The Searchfox indexer fetches those SemanticDB files and turns them into a SCIP index, using scip-semanticdb. That SCIP index is then consumed by the existing Searchfox-internal scip-indexer tool.

In the process, a couple of upstream contributions were made to rust-analyzer (which also emits SCIP data) and scip-semanticdb.

A few examples of Searchfox at work:

If you want to dive into more details, see the feature request on Bugzilla, the implementation and further discussion on GitHub and the release announcement on the mozilla dev-platform mailing list.

Java/C++ Cross-language Support

GeckoView is an Android wrapper around Gecko, the Firefox web engine. It extensively uses cross-language calls between Java and C++.

Searchfox already had support for cross-language interfaces, thanks to its IDL support. We built on top of that to support direct cross-language calls between Java and C++.

First, we identified the different ways the C++ and Java code interact and call each other. There are three ways Java methods marked with the native keyword call into C++:

  • Case A1: By default, the JVM will search for a matching C function to call based on its name. For instance, calling org.mozilla.gecko.mozglue.GeckoLoader.nativeRun from Java will call Java_org_mozilla_gecko_mozglue_GeckoLoader_nativeRun on the C++ side.
  • Case A2: This behavior can be overridden at runtime by calling the JNIEnv::RegisterNatives function on the C++ side to point at another function.
  • Case A3: GeckoView has a code generator that looks for Java items decorated with the @WrapForJNI and native annotations and generates a C++ class template meant to be used through the Curiously Recurring Template Pattern. This template provides an Init static member function that does the right JNIEnv::RegisterNatives calls to bind the Java methods to the implementing C++ class's member functions.

We also identified two ways the C++ code calls Java methods:

  • Case B1: directly with JNIEnv::Call… functions.
  • Case B2: GeckoView's code generator also looks for Java methods marked with @WrapForJNI (without the native keyword this time) and generates a C++ wrapper class and member functions with the right JNIEnv::Call… calls.

Only the C++ side has the complete view of the bindings; so that's where we decided to extract the information from, by extending Mozilla's existing Clang plugin.

First, we defined custom C++ annotations bound_as and binding_to that the clang plugin transforms into the right format for the cross-reference analysis. This means we can manually set the binding information:

class __attribute__((annotate("binding_to", "jvm", "class", "S_jvm_sample/Jni#"))) CallingJavaFromCpp
{
    __attribute__((annotate("binding_to", "jvm", "method", "S_jvm_sample/Jni#javaStaticMethod().")))
    static void javaStaticMethod()
    {
        // Wrapper code
    }

    __attribute__((annotate("binding_to", "jvm", "method", "S_jvm_sample/Jni#javaMethod().")))
    void javaMethod()
    {
        // Wrapper code
    }

    __attribute__((annotate("binding_to", "jvm", "getter", "S_jvm_sample/Jni#javaField.")))
    int javaField()
    {
        // Wrapper code
        return 0;
    }

    __attribute__((annotate("binding_to", "jvm", "setter", "S_jvm_sample/Jni#javaField.")))
    void javaField(int)
    {
        // Wrapper code
    }

    __attribute__((annotate("binding_to", "jvm", "const", "S_jvm_sample/Jni#javaConst.")))
    static constexpr int javaConst = 5;
};

class __attribute__((annotate("bound_as", "jvm", "class", "S_jvm_sample/Jni#"))) CallingCppFromJava
{
    __attribute__((annotate("bound_as", "jvm", "method", "S_jvm_sample/Jni#nativeStaticMethod().")))
    static void nativeStaticMethod()
    {
        // Real code
    }

    __attribute__((annotate("bound_as", "jvm", "method", "S_jvm_sample/Jni#nativeMethod().")))
    void nativeMethod()
    {
        // Real code
    }
};

(This example is, in fact, extracted from our test suite, jni.cpp vs Jni.java.)

Then, we wrote some heuristics that try and identify cases A1 (C functions named Java_…), A3 and B2 (C++ code generated from @WrapForJNI decorators) and automatically generate these annotations. Cases A2 and B1 (manually calling JNIEnv::RegisterNatives or JNIEnv::Call… functions) are rare enough in the Firefox code base and impossible to reliably recognize; so it was decided not to cover them at the time. Developers who wish to declare such bindings could manually annotate them.

After this point, we used Searchfox's existing analysis JSON format and mostly re-used what was already available from IDL support. When triggering the context menu for a binding wrapper or bound function, the definitions in both languages are made available, with “Go to” actions that jump over the generally irrelevant binding internals.

The search results also display both sides of the bridge, for instance:

If you want to dive into more details, see the feature request and detailed problem analysis on Bugzilla, the implementation and further discussion on GitHub, and the release announcement on the Mozilla dev-platform mailing list.

Displaying Interactive Macro Expansions

Aside from this Java/Kotlin-related work, we also added support for displaying and interacting with macro expansions. This was inspired by KDAB's own codebrowser.dev, but improves it to:

  • Display all expansion variants, if they differ across platforms or by definition:
Per-platform-expansions.png

Per-platform expansions

Many-defines-single-expansion-point-use-case.png

Per-definition expansions

  • Make macros fully indexed and interactive:
Full-expansion-with-inner-semantic-analysis-and-context-menu.png

In-macro context menu

This work mainly happened in the Mozsearch Clang plugin to extract macro expansions during the pre-processing stage and index them with the rest of the top-level code.

Again, if you want more details, the feature request is available on Bugzilla and the implementation and further technical discussion is on GitHub.

Summary

Because of the many technologies it makes use of, from compiler plugins and code analyzers written in many languages, to a web front-end written using the usual HTML/CSS/JS, by way of custom tooling and scripts in Rust, Python and Bash, Searchfox is a small but complex and really interesting project to work on. KDAB successfully added Java/Kotlin code indexing, including analyzing their C++ bindings, and are starting to improve Searchfox's C++ support itself, first with fully-indexed macro expansions and next with improved templates support.

The post Improvements to Mozilla's Searchfox Code Browser appeared first on KDAB.

Setting C++ Defines with CMake

by David Faure (KDAB)

Setting C++ Defines with CMake

The goal

When building C++ code with CMake, it is very common to want to set some pre-processor defines in the CMake code.

For instance, we might want to set the project's version number in a single place, in CMake code like this:

project(MyApp VERSION 1.5)

This sets the CMake variable PROJECT_VERSION to 1.5, which we can then use to pass -DMYAPP_VERSION_STRING=1.5 to the C++ compiler. The about dialog of the application can then use this to show the application version number, like this:

const QString aboutString = QStringLiteral("My App version: %1").arg(MYAPP_VERSION_STRING);
  QMessageBox::information(this, "My App", aboutString);

Similarly, we might have a boolean CMake option like START_MAXIMIZED, which the user compiling the software can set to ON or OFF:

option(START_MAXIMIZED "Show the mainwindow maximized" OFF)

If it's ON, you would pass -DSTART_MAXIMIZED, otherwise nothing. The C++ code will then use #ifdef. (We'll see that there's a better way.)

#ifdef START_MAXIMIZED
      w.showMaximized();
  #else
      w.show();
  #endif

The common (but suboptimal) solution

A solution that many people use for this is the CMake function add_definitions. It would look like this:

add_definitions(-DMYAPP_VERSION_STRING="${PROJECT_VERSION}")
  if (START_MAXIMIZED)
     add_definitions(-DSTART_MAXIMIZED)
  endif()

Technically, this works but there are a number of issues.

First, add_definitions is deprecated since CMake 3.12 and add_compile_definitions should be used instead, which allows to remove the leading -D.

More importantly, there's a major downside to this approach: changing the project version or the value of the boolean option will force CMake to rebuild every single .cpp file used in targets defined below these lines (including in subdirectories). This is because add_definitions and add_compile_definitions ask to pass -D to all cpp files, instead of only those that need it. CMake doesn't know which ones need it, so it has to rebuild everything. On large real-world projects, this could take something like one hour, which is a major waste of time.

A first improvement we can do is to at least set the defines to all files in a single target (executable or library) instead of "all targets defined from now on". This can be done like this:

target_compile_definitions(myapp PRIVATE MYAPP_VERSION_STRING="${PROJECT_VERSION}")
  if(START_MAXIMIZED)
     target_compile_definitions(myapp PRIVATE START_MAXIMIZED)
  endif()

We have narrowed the rebuilding effect a little bit, but are still rebuilding all cpp files in myapp, which could still take a long time.

The recommended solution

There is a proper way to do this, such that only the files that use these defines will be rebuilt; we simply have to ask CMake to generate a header with #define in it and include that header in the few cpp files that need it. Then, only those will be rebuilt when the generated header changes. This is very easy to do:

configure_file(myapp_config.h.in myapp_config.h)

We have to write the input file, myapp_config.h.in, and CMake will generate the output file, myapp_config.h, after expanding the values of CMake variables. Our input file would look like this:

#define MYAPP_VERSION_STRING "${PROJECT_VERSION}"
  #cmakedefine01 START_MAXIMIZED

A good thing about generated headers is that you can read them if you want to make sure they contain the right settings. For instance, myapp_config.h in your build directory might look like this:

#define MYAPP_VERSION_STRING "1.5"
  #define START_MAXIMIZED 1

For larger use cases, we can even make this more modular by moving the version number to another input file, say myapp_version.h.in, so that upgrading the version doesn't rebuild the file with the showMaximized() code and changing the boolean option doesn't rebuild the about dialog.

If you try this and you hit a "file not found" error about the generated header, that's because the build directory (where headers get generated) is missing in the include path. You can solve this by adding set(CMAKE_INCLUDE_CURRENT_DIR TRUE) near the top of your CMakeLists.txt file. This is part of the CMake settings that I recommend should always be set; you can make it part of your new project template and never have to think about it again.

There's just one thing left to explain: what's this #cmakedefine01 thing?

If your C++ code uses #ifdef, you want to use #cmakedefine, which either sets or doesn't set the define. But there's a major downside of doing that -- if you forget to include myapp_config.h, you won't get a compile error; it will just always go to the #else code path.

We want a solution that gives an error if the #include is missing. The generated header should set the define to either 0 or 1 (but always set it), and the C++ code should use #if. Then, you get a warning if the define hasn't been set and, because people tend to ignore warnings, I recommend that you upgrade it to an error by adding the compiler flag -Werror=undef, with gcc or clang.  Let me know if you are aware of an equivalent flag for MSVC.

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    target_compile_options(myapp PRIVATE -Werror=undef)
  endif()

And these are all the pieces we need. Never use add_definitions or add_compile_definitions again for things that are only used by a handful of files. Use configure_file instead, and include the generated header. You'll save a lot of time compared to recompiling files unnecessarily.

I hope this tip was useful.

For more content on CMake, we curated a collection of resources about CMake with or without Qt. Check out the videos.

The post Setting C++ Defines with CMake appeared first on KDAB.

KDAB Training Day - May 8th, 2025

by Editor Team (KDAB)

KDAB Training Day - May 8th, 2025

Early-Bird tickets are on sale for the KDAB Training Day 2025 until 2025-03-31 23:59

training-day-long-banner.png

The KDAB Training Day 2025 will take place in Munich on May 8th, right after the Qt World Summit on May 6th-7th. Choose to buy a combo ticket here (for access to QtWS and Training Day) or here (for access to Training Day only).

Seats are limited, so don't wait too long if you want to participate in a specific course. Tickets include access to the selected training course, training material, lunch buffet, beverages, and coffee breaks. Note: The Training Day is held at Hotel NH Collection München Bavaria, located at the Munich Central Station (not the same location as Qt World Summit).

Get your ticket

Why should you attend the KDAB Training Day?

With over 20 years of experience and a rich store of well-structured, constantly updated training material, KDAB offers hands-on, practical programming training in Qt/QMLModern C++3D/OpenGLDebugging & Profiling, and lately Rust - both for beginners as well as experienced developers.

All courses provided at the Training Day include central parts of the regular 3- to 4-day courses available as scheduled or customized on-site training. Choosing a compact, learning-rich one-day course, lets you experience the quality and effectiveness of KDAB’s usual training offerings.

Courses available at the KDAB Training Day 2025

  • QML Application Architecture
  • QML/C++ integration
  • Modern C++ Paradigms
  • Integrating Rust into Qt applications
  • Effective Modern QML
  • Integrating Custom 3D Renderers with Qt Applications

QML Application Architecture

In this training, we do a step-by-step walkthrough of how to build a QML-based embedded application from the ground up and discuss some challenges that are typically met along the way.

An important part of that journey is an investigation of where to put the boundaries between what you do in C++ and what you do in QML. We also look at some of the tools and building blocks we have available in QML that can help us achieve well-performing, well-structured, and well-maintainable applications.

This course is for

(Qt) developers looking to improve their understanding of how to construct maintainable and efficient larger-scale QML applications.

Prerequisite

Some real-world experience working on QML applications as well as a basic understanding of Qt and C++.

Get your ticket

QML/C++ Integration

In this training, we start with a recap of fundamentals:

  • How do we expose C++ API to QML?
  • How do we make data available to QML?

Afterward, we explore several more advanced techniques, often widely deployed within Qt's QML modules, such as Qt Quick.

This will answer questions such as:

  • How would I do a Loader like component?
  • How would I do a Layout like component?

This course is for

Qt/QML developers who are familiar with the QML APIs of QtQuick and related modules and who have wondered how these are implemented and want to use similar techniques in their project-specific APIs.

Prerequisite

Some real-world experience working on QML applications as well as a basic understanding of Qt and C++.

Get your ticket

Modern C++ Paradigms

Modern C++ emphasizes safer, more efficient, and maintainable code through higher-level abstractions that reduce error-prone manual work.

This training will explore key paradigms shaping recent C++ evolution, starting with value semantics in class design, which enhances code safety, local reasoning, and thread safety. We will examine modern C++ tools for creating value-oriented types, including move semantics, smart pointers, and other library enablers.

Next, we will look at expressive, type and value-based error handling.

Finally, we'll cover range-based programming, which enables clean, declarative code and unlocks new patterns through lazy, composable transformations.

This course is for

C++ developers who wish to improve the quality of their code, in particular those who wish to write future-proof APIs.

Prerequisites

Prior professional experience in C++. Experience with the latest C++ standards (C++20/23/26) is a plus. We will use several examples inspired by Qt APIs, so Qt knowledge is also a plus (but this is not going to be a Qt training).

Get your ticket

Integrating Rust into Qt Applications

In this step-by-step course, we start with a Qt/C++ application and add Rust code to it piece by piece. To achieve this, we will cover:

  • Use of Cargo (Rusts build system) with CMake
  • Accessing Rust code from C++ with CXX (and vice-versa)
  • Defining your own QObject types in Rust with CXX-Qt

We discuss when to use Rust compared to C++ to make the best of both languages and how to use them together effectively to make Qt applications safer and easier to maintain.

This course is for

Qt/C++ Developers with an interest in Rust who want to learn how to use Rust in their existing applications.

Prerequisites

Basic Qt/C++ knowledge, as well as basic Rust knowledge, is required. A working Qt installation with CMake and a working Rust installation is needed. We will provide material before the training day that participants should use to check their setup before the training.

Get your ticket

Effective Modern QML

In this training, we look into all the new developments in QML over the last few years and how they lead to more expressive, performant, and maintainable code.

This includes:
- The qt_add_qml_module CMake API
- Declarative type registration
- The different QML compilers
- New language and library features
- New developments in Qt Quick Controls
- Usage of tools like qmllint, QML Language Server, and qmlformat

The focus will be on gradually modernizing existing codebases with new tools and practices.

This course is for

Developers who learned QML back in the days of Qt 5 and want to catch up with recent developments in QML and modernize their knowledge as well as codebases.

Prerequities

Some real-world experience with QML and a desire to learn about modern best practices.

Get your ticket

Integrating Custom 3D Renderers with Qt Applications

Qt has long offered ways of using low-level 3d libraries such as OpenGL to do custom rendering. Whether at the Window, the Widget, or Quick Item level, the underlying rendering system can be accessed in ways that make it safe to integrate such 3rd party renderers. This remains true in the Qt 6 timeline, although the underlying rendering system has changed and OpenGL has been replaced by RHI.

In this course, we look at how the graphic stack is structured in Qt 6 and how third-party renderers can be integrated on the various platforms supported by Qt.

We then focus on the specific case of integrating Vulkan-based renderers. Vulkan is the successor to OpenGL; it's much more powerful but harder to learn. To facilitate the initial use of Vulkan, we introduce KDGpu, a library that encapsulates Vulkan while preserving the underlying concepts of pipeline objects, buffer handling, synchronization, etc.

This course is for

This course targets developers wanting to understand the recent state of the graphics stack in Qt, discover the fundamental principles of modern graphics API, and integrate their custom renderers in their applications.

Prerequisite

Prior knowledge of Qt will be required. A basic understanding of 3d graphics would be beneficial.

Get your ticket

Video from KDAB Training Day 2023 held in Berlin

The post KDAB Training Day - May 8th, 2025 appeared first on KDAB.

CXX-Qt 0.7 Release

CXX-Qt 0.7 Release

We just released CXX-Qt version 0.7!

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

For 0.7, we have stabilized the cxx-qt bridge macro API and there have been many internal refactors to ensure that we have a consistent baseline to support going forward. We encourage developers to reach out if they find any unclear areas or missing features, to help us ensure a roadmap for them, as this may be the final time we can adapt the API. In the next releases, we're looking towards stabilizing the cxx-qt-build and getting the cxx-qt-lib APIs ready for 1.0.

Check out the new release through the usual channels:

Some of the most notable developer-facing changes:

Stabilized #[cxx_qt::bridge] macro

CXX-Qt 0.7 reaches a major milestone by stabilizing the bridge macro that is at the heart of CXX-Qt.
You can now depend on your CXX-Qt bridges to remain compatible with future CXX-Qt versions.
As we're still pre-1.0, we may still introduce very minor breaking changes to fix critical bugs in the edge-cases of the API, but the vast majority of bridges should remain compatible with future versions.

This stabilization is also explicitly limited to the bridge API itself. Breaking changes may still occur in e.g. cxx-qt-lib, cxx-qt-build, and cxx-qt-cmake. We plan to stabilize those crates in the next releases.

Naming Changes

The handling of names internally has been refactored to ensure consistency across all usages. During this process, implicit automatic case conversion has been removed, so cxx_name and rust_name are now used to specify differing Rust and C++ names. Since the automatic case conversion is useful, it can be explicitly enabled using per extern block attributes auto_cxx_name and auto_rust_name, while still complimenting CXX. For more details on how these attributes can be used, visit the attributes page in the CXX-Qt book.

// with 0.6 implicit automatic case conversion
#[cxx_qt::bridge]
mod ffi {
  unsafe extern "RustQt" {
    #[qobject]
    #[qproperty(i32, my_number) // myNumber in C++
    type MyObject = super::MyObjectRust;

    fn my_method(self: &MyObject); // myMethod in C++
  }
}

// with 0.7 cxx_name / rust_name
#[cxx_qt::bridge]
mod ffi {
  unsafe extern "RustQt" {
    #[qobject]
    #[qproperty(i32, my_number, cxx_name = "myNumber")
    type MyObject = super::MyObjectRust;

    #[cxx_name = "myMethod"]
    fn my_method(self: &MyObject);
  }
}

// with 0.7 auto_cxx_name / auto_rust_name
#[cxx_qt::bridge]
mod ffi {
  #[auto_cxx_name] // <-- enables automatic cxx_name generation within the `extern "RustQt"` block
  unsafe extern "RustQt" {
    #[qobject]
    #[qproperty(i32, my_number) // myNumber in C++
    type MyObject = super::MyObjectRust;

    fn my_method(self: &MyObject); // myMethod in C++
  }
}

cxx_file_stem Removal

In previous releases, the output filename of generated C++ files used the cxx_file_stem attribute of the CXX-Qt bridge. This has been changed to use the filename of the Rust source file including the directory structure.

Previously, the code below would generate a C++ header path of my_file.cxxqt.h. After the changes, the cxx_file_stem must be removed and the generated C++ header path changes to crate-name/src/my_bridge.cxxqt.h. This follows a similar pattern to CXX.

// crate-name/src/my_bridge.rs

// with 0.6 a file stem was specified
#[cxx_qt::bridge(cxx_file_stem = "my_file")]
mod ffi {
...
}

// with 0.7 the file path is used
#[cxx_qt::bridge]
mod ffi {
...
}

Build System Changes

The internals of the build system have changed so that dependencies are automatically detected and configured by cxx-qt-build, libraries can pass build information to cxx-qt-build, and a CXX-Qt CMake module is now available providing convenience wrappers around corrosion. This means that the cxx-qt-lib-headers crate has been removed and only cxx-qt-lib is required. With these changes, there is now no need for the -header crates that existed before. Previously, some features were enabled by default in cxx-qt-lib. Now these are all opt-in. We have provided full and qt_full as convenience to enable all features; however, we would recommend opting in to the specific features you need.

We hope to improve the API of cxx-qt-build in the next cycle to match the internal changes and become more modular.

Further Improvements

CXX-Qt can now be successfully built for WASM, with documented steps available in the book and CI builds for WASM to ensure continued support.

Locking generation on the C++ side for all methods has been removed, which simplifies generation and improves performance. Using queue from cxx_qt::CxxQtThread is still safe, as it provides locking, but it is up to the developer to avoid incorrect multi-threading in C++ code (as in the CXX crate). Note that Qt generally works well here, with the signal/slot mechanism working safely across threads.

As with most releases, there are more Qt types wrapped in cxx-qt-lib and various other changes are detailed in the CHANGELOG.

Make sure to subscribe to the KDAB YouTube channel, where we'll post more videos on CXX-Qt in the coming weeks.

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

  • Ben Ford
  • Laurent Montel
  • Matt Aber
  • knox (aka @knoxfighter)
  • Be Wilson
  • Joshua Goins
  • Alessandro Ambrosano
  • Alexander Kiselev
  • Alois Wohlschlager
  • Darshan Phaldesai
  • Jacob Alexander
  • Sander Vocke

The post CXX-Qt 0.7 Release appeared first on KDAB.