Qt 6.9.1 Released

We are excited to announce the release of Qt 6.9.1!  As a patch release, Qt 6.9.1 does not introduce new features but delivers over 450 bug fixes, security updates, and enhancements on top of the Qt 6.9.0 release.  For a detailed breakdown of the most significant changes, refer to the  Qt 6.9.1 release notes.

Interactive Plots with PySide6

Interactive Plots with PySide6

Nowadays it is getting more and more popular to write Qt applications in Python using a binding module like PySide6. One reason for this is probably Python's rich data science ecosystem which makes it a breeze to load and visualize complex datasets. In this article we focus (although not exclusively) on the widespread plotting library Matplotlib: We demonstrate how you can embed it in PySide applications and how you can customize the default look and feel to your needs. We round off the article with an outlook into Python plotting libaries beyond Matplotlib and their significance for Qt.

Continue reading Interactive Plots with PySide6 at basysKom GmbH.

Why the Micro-Mobility Sector is Betting on Robust and High-Performance Systems

Industry Insights Blog Series

 

 

Toni Paila

Director, Qt for Microcontrollers

Sumitabh Ghosh

Senior Product Lead, MCU 

 

Richer functionality and higher performance in two-wheeler displays—while maintaining a competitive price point—is the balance micro-mobility companies need to maintain as they try to win over customers.

Two-wheelers in the same category sell at very similar price points. What ultimately makes or breaks the deal are the features and quality that manufacturers can deliver at a given price point. Rising demand from increasingly tech-savvy consumers has driven the micro-mobility industry to offer features previously seen only in cars. Collision avoidance, advanced rider assistance systems, navigation, and real-time diagnostics are all making their way onto two-wheelers and e-bikes.

That said, getting such advanced features to run on resource-constrained embedded devices is a challenge. Achieving near-real-time performance adds to this complexity. Riders make snap judgments based on how fast their display responds, how clearly information is presented, and how user-friendly everything feels. For OEMs and tier-one suppliers, the challenge lies in providing this high performance on a budget.

When a customer compares two models side by side, priced similarly, they will pick the one with a faster, sharper, and more responsive UI. Performance becomes a critical differentiator, even if the hardware budgets are almost identical.

In the first place, cost constraints dictate strict hardware limitations. The embedded devices used for two-wheelers and micro-mobility need to snap-boot but have only scarce computational resources to run several functions and services in parallel. Nonetheless, the final performance of the application and the quality of its graphics are not dictated solely by the hardware. Lightweight graphics, hardware acceleration, and optimized software can deliver advanced functionality even on highly constrained microcontrollers (MCUs).

On the tooling side, a persistent challenge in delivering high-performance systems is fragmentation. Each hardware vendor comes with its own software development kits (SDKs), and developers must juggle multiple vendor SDKs, each with their own quirks. All this comes with associated costs. 

Developing Qt applications with CLion

Developing with Qt is becoming even more accessible across your favourite tools and editors. We want you to stay focused on your work, in the tooling you’re most comfortable with – be it Qt Creator, CLion, Visual Studio Code, or even a simple text editor such as Vim. That’s why we’d love to show you how you can develop your Qt projects in CLion.

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.

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.

Which Python GUI library should you use? — Comparing the Python GUI libraries available in 2025

Python is a popular programming used for everything from scripting routine tasks to building websites and performing complex data analysis. While you can accomplish a lot with command line tools, some tasks are better suited to graphical interfaces. You may also find yourself wanting to build a desktop front-end for an existing tool to improve usability for non-technical users. Or maybe you're building some hardware or a mobile app and want an intuitive touchscreen interface.

To create graphical user interfaces with Python, you need a GUI library. Unfortunately, at this point things get pretty confusing -- there are many different GUI libraries available for Python, all with different capabilities and licensing. Which Python GUI library should you use for your project?

In this article, we will look at a selection of the most popular Python GUI frameworks currently available and why you should consider using them for your own projects. You'll learn about the relative strengths of each library, understand the licensing limitations and see a simple Hello, World! application written in each. By the end of the article you should feel confident choosing the right library for your project.

tldr If you're looking to build professional quality software, start with PySide6 or PyQt6. The Qt framework is batteries-included — whatever your project, you'll be able to get it done. We have a complete PySide6 tutorial and PyQt6 tutorial.

There is some bias here -- I have been working with Qt for about 10 years, writing books & developing commercial software. But I chose it and continue to use it because it's the best option for getting things done. That said, the other libraries all have their place & may be a better fit for your own project -- read on to find out!

If you're looking for more demos to see what you can do with Python, we have a Github respository full of Python GUI examples to get you started.

PyQt or PySide

Best for commercial, multimedia, scientific or engineering desktop applications. Best all-rounder with batteries included.

PyQt and PySide are wrappers around the Qt framework. They allow you to easily create modern interfaces that look right at home on any platform, including Windows, macOS, Linux and even Android. They also have solid tooling with the most notable being Qt Creator, which includes a WYSIWYG editor for designing GUI interfaces quickly and easily. Being backed by a commercial project means that you will find plenty of support and online learning resources to help you develop your application.

Qt (and by extension PyQt & PySide) is not just a GUI library, but a complete application development framework. In addition to standard UI elements, such as widgets and layouts, Qt provides MVC-like data-driven views (spreadsheets, tables), database interfaces & models, graph plotting, vector graphics visualization, multimedia playback, sound effects & playlists and built-in interfaces for hardware such as printing. The Qt signals and slots models allows large applications to be built from re-usable and isolated components.

While other toolkits can work great when building small & simple tools, Qt really comes into its own for building real commercial-quality applications where you will benefit from the pre-built components. This comes at the expense of a slight learning curve. However, for smaller projects Qt is not really any more complex than other libraries. Qt Widgets-based applications use platform native widgets to ensure they look and feel at home on Windows, macOS and Qt-based Linux desktops.

Installation pip install pyqt6 or pip install pyside6

A simple hello world application in PyQt6, using the Qt Widgets API is shown below.

python
from PyQt6.QtWidgets import QMainWindow, QApplication, QPushButton

import sys


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

        self.setWindowTitle("Hello World")

        button = QPushButton("My simple app.")
        button.pressed.connect(self.close)

        self.setCentralWidget(button)
        self.show()


app = QApplication(sys.argv)
w = MainWindow()
app.exec()
python
from PySide6.QtWidgets import QMainWindow, QApplication, QPushButton

import sys


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

        self.setWindowTitle("Hello World")

        button = QPushButton("My simple app.")
        button.pressed.connect(self.close)

        self.setCentralWidget(button)
        self.show()


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

As you can see, the code is almost identical between PyQt & PySide, so it's not something to be concerned about when you start developing with either: you can always migrate easily if you need to.

PyQt6 Application Screenshot Hello world application built using PyQt6, running on Windows 11

Before the Qt Company (under Nokia) released the officially supported PySide library in 2009, Riverbank Computing had released PyQt in 1998. The main difference between these two libraries is in licensing.

  • PyQt The free-to-use version of PyQt is licensed under GNU General Public License (GPL) v3. This means that you must also license your applications with the GPL unless you purchase a commercial version
  • PySide is licensed under GNU Lesser General Public License (LGPL). This means PySide may be used in your applications without any additional fee.

Note that both these libraries are separate from Qt itself which also has a free-to-use, open source version and a paid, commercial version.

For a more information see our article on PyQt vs PySide licensing. tldr If you want to develop closed-source software without paying for a license, use PySide6.

Tkinter

Best for simple portable Python GUIs and applications

Tkinter is the defacto GUI framework for Python. It comes bundled with Python on both Windows and macOS. (On Linux, it may require downloading an additional package from your distribution's repo.) Tkinter is a wrapper written around the Tk GUI toolkit. Its name is an amalgamation of the words Tk and Interface.

Tkinter is a simple library with support for standard layouts and widgets, as well as more complex widgets such as tabbed views & progressbars. Tkinter is a pure GUI library, not a framework. There is no built-in support for GUIs driven from data sources, databases, or for displaying or manipulating multimedia or hardware. However, if you need to make something simple that doesn't require any additional dependencies, Tkinter may be what you are looking for. Tkinter is cross-platform however the widgets can look outdated, particularly on Windows.

Installation Already installed with Python on Windows and macOS. Ubuntu/Debian Linux sudo apt install python3-tk

A simple hello world application in Tkinter is shown below.

python
import tkinter as tk

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button = tk.Button(text="My simple app.")
button.bind("", handle_button_press)
button.pack()

# Start the event loop.
window.mainloop()
python
from tkinter import Tk, Button


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

        self.title("Hello World")

        self.button = Button(text="My simple app.")
        self.button.bind("", self.handle_button_press)
        self.button.pack()

    def handle_button_press(self, event):
        self.destroy()


# Start the event loop.
window = Window()
window.mainloop()

Tkinter Application Screenshot Hello world application built using Tkinter, running on Windows 11

Tkinter was originally developed by Steen Lumholt and Guido Van Rossum, who designed Python itself. Both the GUI framework and the language are licensed under the same Python Software Foundation (PSF) License. While the license is compatible with the GPL, it is a 'permissive' license (similar to the MIT License) that allows it to be used for proprietary applications and modifications.

Kivy

Best for Python mobile app development

While most other GUI frameworks are bindings to toolkits written in other programming languages, Kivy is perhaps the only framework which is primarily written in pure Python. If you want to create touchscreen-oriented interfaces with a focus on mobile platforms such as Android and iOS, this is the way to go. This does run on desktop platforms (Windows, macOS, Linux) as well but note that your application may not look and behave like a native application. However, there is a pretty large community around this framework and you can easily find resources to help you learn it online.

The look and feel of Kivy is extremely customizable, allowing it to be used as an alternative to libraries like Pygame (for making games with Python). The developers have also released a number of separate libraries for Kivy. Some provide Kivy with better integration and access to certain platform-specific features, or help package your application for distribution on platforms like Android and iOS. Kivy has it's own design language called Kv, which is similar to QML for Qt. It allows you to easily separate the interface design from your application's logic.

There is a 3rd party add-on for Kivy named KivyMD that replaces Kivy's widgets with ones that are compliant with Google's Material Design.

A simple hello world application in Kivy is shown below.

Installation pip install kivy

A simple hello world application in Kivy is shown below.

python
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.core.window import Window

Window.size = (300, 200)


class MainWindow(BoxLayout):
    def __init__(self):
        super().__init__()
        self.button = Button(text="Hello, World?")
        self.button.bind(on_press=self.handle_button_clicked)

        self.add_widget(button)

    def handle_button_clicked(self, event):
        self.button.text = "Hello, World!"


class MyApp(App):
    def build(self):
        self.title = "Hello, World!"
        return MainWindow()


app = MyApp()
app.run()

Kivy Application Screenshot Hello world application built using Kivy, running on Windows 11

An equivalent application built using the Kv declarative language is shown below.

python
import kivy
kivy.require('1.0.5')

from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty


class Controller(FloatLayout):
    '''Create a controller that receives a custom widget from the kv lang file.

    Add an action to be called from the kv lang file.
    '''
    def button_pressed(self):
        self.button_wid.text = 'Hello, World!'


class ControllerApp(App):

    def build(self):
        return Controller()


if __name__ == '__main__':
    ControllerApp().run()
python
#:kivy 1.0

:
    button_wid: custom_button

    BoxLayout:
        orientation: 'vertical'
        padding: 20

        Button:
            id: custom_button
            text: 'Hello, World?'
            on_press: root.button_pressed()

The name of the Kv file must match the name of the class from the main application -- here Controller and controller.kv.

Kivy Kv Application Screenshot Hello world application built using Kivy + Kv, running on Windows 11

In February 2011, Kivy was released as the spiritual successor to a similar framework called PyMT. While they shared similar goals and was also led by the current core developers of Kivy, where Kivy differs is in its underlying design and a professional organization which actively develops and maintains it. Kivy is licensed under the MIT license, which is a 'permissive' license that allows you to use it freely in both open source and proprietary applications. As such, you are even allowed to make proprietary modifications to the framework itself.

BeeWare Toga

Best for simple cross-platform native GUI development

BeeWare is a collection of tools and libraries which work together to help you write cross platform Python applications with native GUIs. That means, the applications you create use the OS-provided widgets and behaviors, appearing just like any other application despite being written in Python.

The BeeWare system includes -

  • Toga a cross platform widget toolkit, which we'll demo below.
  • Briefcase a tool for packaging Python projects as distributable artefacts.
  • Libraries for accessing platform-native APIs.
  • Pre-compiled builds of Python for platforms where official Python installers aren't avaiable.

Installation pip install toga

A simple hello world application using BeeWare/Toga is shown below.

python
import toga
from toga.style import Pack


class HelloWorld(toga.App):
    def startup(self):
        layout = toga.Box()

        self.button = toga.Button(
            "Say Hello!",
            on_press=self.say_hello,
            style=Pack(margin=5),
        )
        layout.add(self.button)

        self.main_window = toga.MainWindow(title="Hello world!")
        self.main_window.content = layout
        self.main_window.show()

    def say_hello(self, source_widget):
        # Receives the button that was clicked.
        source_widget.text = "Hello, world!"


app = HelloWorld(formal_name="Hello, world!", app_id="hello.world")
app.main_loop()

Toga Application Screenshot Hello world application built using BeeWare Toga, running on Windows 11

BeeWare is developed by Russell Keith-Magee. It is licensed under the BSD 3-Clause "New" or "Revised" License. The above example is using a single Python file for simplicity, see the linked tutorial below to see how to set up an application using Briefcase for cross-platform deployment.

PyQt/PySide with QML

Best for Raspberry Pi, microcontrollers, industrial and consumer electronics

When using PyQt and PySide you actually have two options for building your GUIs. We've already introduced the Qt Widgets API which is well-suited for building desktop applications. But Qt also provides a declarative API in the form of Qt Quick/QML.

Using Qt Quick/QML you have access to the entire Qt framework for building your applications. Your UI consists of two parts: the Python code which handles the business logic and the QML which defines the structure and behavior of the UI itself. You can control the UI from Python, or use embedded Javascript code to handle events and animations.

Qt Quick/QML is ideally suited for building modern touchscreen interfaces for microcontrollers or device interfaces -- for example, building interfaces for microcontrollers like the Raspberry Pi. However you can also use it on desktop to build completely customized application experiences, like those you find in media player applications like Spotify, or to desktop games.

Installation pip install pyqt6 or pip install pyside6

A simple Hello World app in PyQt6 with QML. Save the QML file in the same folder as the Python file, and run as normally.

python
import sys

from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine


app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load('main.qml')

sys.exit(app.exec())
qml
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 600
    height: 500
    title: "HelloApp"

    Text {
        anchors.centerIn: parent
        text: "Hello World"
        font.pixelSize: 24
    }

}

Licensing for Qt Quick/QML applications is the same as for other PyQt/PySide apps.

PyQt6 QML Application Screenshot Hello world application built using PyQt6 & QML, running on Windows 11

PySimpleGUI

PySimpleGUI is no longer being developed. We do not recommend it if you are starting a new project in 2025. Take a look at PySide6 instead.

PySimpleGUI aims to simplify GUI application development for Python. It doesn't reinvent the wheel but provides a wrapper around other existing frameworks such as Tkinter, Qt (PySide 2), WxPython and Remi. By doing so, it lowers the barrier to creating a GUI but also allows you to easily migrate from one GUI framework to another by simply changing the import statement.

While there is a separate port of PySimpleGUI for each of these frameworks, the Tkinter version is considered the most feature complete. Wrapping other libraries comes at a cost however — your applications will not be able to exploit the full capabilities or performance of the underlying libraries. The pure-Python event loop can also hinder performance by bottlenecking events with the GIL. However, this is only really a concern when working with live data visualization, streaming or multimedia applications.

There is a fair amount of good resources to help you learn to use PySimpleGUI, including an official Cookbook and a Udemy course offered by the developers themselves. According to their project website, PySimpleGUI was initially made (and later released in 2018) because the lead developer wanted a 'simplified' GUI framework to use in his upcoming project and wasn't able to find any that met his needs.

Installation pip install pysimplegui

python
import PySimpleGUI as sg


layout = [
    [sg.Button("My simple app.")]
]

window = sg.Window("Hello World", layout)

while True:
    event, values = window.read()
    print(event, values)
    if event == sg.WIN_CLOSED or event == "My simple app.":
        break

window.close()

PySimpleGUI Application Screenshot Hello world application built using PySimpleGUI, running on Windows 11

PySimpleGUI 4 was licensed under the same LGPL v3 license as PySide. PySimpleGUI 5 switched to a paid commercial license model. There is a fork of PySimpleGUI 4 called FreeSimpleGUI which retains the original LGPL license.

WxPython

Best for simple portable desktop applications

WxPython is a wrapper for the popular, cross-platform GUI toolkit called WxWidgets. It is implemented as a set of Python extension modules that wrap the GUI components of the popular wxWidgets cross platform library, which is written in C++.

WxPython uses native widgets on most platforms, ensure that your application looks and feels at home. However, WxPython is known to have certain platform-specific quirks and it also doesn't provide the same level of abstraction between platforms as Qt for example. This may affect how easy it is to maintain cross-platform compatibility for your application.

WxPython is under active development and is also currently being reimplemented from scratch under the name 'WxPython Phoenix'. The team behind WxWidgets is also responsible for WxPython, which was initially released in 1998.

Installation pip install wxpython

python
import wx


class MainWindow(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(200, -1))

        self.button = wx.Button(self, label="My simple app.")
        self.Bind(
            wx.EVT_BUTTON, self.handle_button_click, self.button
        )

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.button)

        self.SetSizer(self.sizer)
        self.SetAutoLayout(True)
        self.Show()

    def handle_button_click(self, event):
        self.Close()


app = wx.App(False)
w = MainWindow(None, "Hello World")
app.MainLoop()

WxPython Application Screenshot Hello world application built using WxPython, running on Windows 11

Both WxWidgets and WxPython are licensed under a WxWindows Library License, which is a 'free software' license similar to LGPL (with a special exception). It allows both proprietary and open source applications to use and modify WxPython.

PyGObject (GTK+)

Best for developing applications for GNOME desktop

If you intend to create an application that integrates well with GNOME and other GTK-based desktop environments for Linux, PyGObject is the right choice. PyGObject itself is a language-binding to the GTK+ widget toolkit. It allows you to create modern, adaptive user interfaces that conform to GNOME's Human Interface Guidelines (HIG).

It also enables the development of 'convergent' applications that can run on both Linux desktop and mobile platforms. There are a few first-party and community-made, third-party tools available for it as well. This includes the likes of GNOME Builder and Glade, which is yet another WYSIWYG editor for building graphical interfaces quickly and easily.

Unfortunately, there aren't a whole lot of online resources to help you learn PyGObject application development, apart from this one rather well-documented tutorial. While cross-platform support does exist (e.g. Inkscape, GIMP), the resulting applications won't feel completely native on other desktops. Setting up a development environment for this, especially on Windows and macOS, also requires more steps than for most other frameworks in this article, which just need a working Python installation.

Installation Ubuntu/Debian sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-4.0, macOS Homebrew brew install pygobject4 gtk+4

python
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk

def on_activate(app):
    win = Gtk.ApplicationWindow(application=app)
    btn = Gtk.Button(label="Hello, World!")
    btn.connect('clicked', lambda x: win.close())
    win.set_child(btn)
    win.present()

app = Gtk.Application(application_id='org.gtk.Example')
app.connect('activate', on_activate)
app.run(None)

PyGObject Application Screenshot Hello world application built using PyGObject, running on Ubuntu Linux 21.10

PyGObject is developed and maintained under the GNOME Foundation, who is also responsible for the GNOME desktop environment. PyGObject replaces several separate Python modules, including PyGTK, GIO and python-gnome, which were previously required to create a full GNOME/GTK application. Its initial release was in 2006 and it is licensed under an older version of LGPL (v2.1). While there are some differences with the current version of LGPL (v3), the license still allows its use in proprietary applications but requires any modification to the library itself to be released as open source.

Remi

Best for web based UIs for Python applications

Remi, which stands for REMote Interface, is the ideal solution for applications that are intended to be run on servers and other headless setups. (For example, on a Raspberry Pi.) Unlike most other GUI frameworks/libraries, Remi is rendered completely in the browser using a built-in web server. Hence, it is completely platform-independent and runs equally well on all platforms.

That also makes the application's interface accessible to any computer or device with a web browser that is connected to the same network. Although access can be restricted with a username and password, it doesn't implement any security strategies by default. Note that Remi is meant to be used as a desktop GUI framework and not for serving up web pages. If more than one user connects to the application at the same time, they will see and interact with the exact same things as if a single user was using it.

Remi requires no prior knowledge of HTML or other similar web technologies. You only need to have a working understanding of Python to use it, which is then automatically translated to HTML. It also comes included with a drag n drop GUI editor that is akin to Qt Designer for PyQt and PySide.

python
import remi.gui as gui
from remi import start, App

class MyApp(App):

    def main(self):
        container = gui.VBox(width=120, height=100)

        # Create a button, with the label "Hello, World!"
        self.bt = gui.Button('Hello, World?')
        self.bt.onclick.do(self.on_button_pressed)

        # Add the button to the container, and return it.
        container.append(self.bt)
        return container

    def on_button_pressed(self, widget):
        self.bt.set_text('Hello, World!')

start(MyApp)

Remi is licensed under the Apache License v2.0, which is another 'permissive' license similar to the MIT License. The license allows using it in both open source and proprietary applications, while also allowing proprietary modifications to be made to the framework itself. Its main conditions revolve around the preservation of copyright and license notices.

Remi Application Screenshot Hello world application built using Remi, running on Chrome on Windows 11

Conclusion

If you're looking to build GUI applications with Python, there is probably a GUI framework/library listed here that fits the bill for your project. Try and weigh up the capabilities & licensing of the different libraries with the scale of your project, both now and in the future.

Don't be afraid to experiment a bit with different libraries, to see which feel the best fit. While the APIs of GUI libraries are very different, they share many underlying concepts in common and things you learn in one library will often apply in another.

You are only limited by your own imagination. So go out there and make something!

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.