Qt on macOS 26 Tahoe
The yearly release of Apple's operating systems, including macOS 26 Tahoe, are just around the corner, and we've been busy weeding out issues in Qt to prepare for the final release. Here's what you need to know.

The yearly release of Apple's operating systems, including macOS 26 Tahoe, are just around the corner, and we've been busy weeding out issues in Qt to prepare for the final release. Here's what you need to know.
The latest patch release of Qt for Android Automotive 6.9.2 was released. This release is based on Qt 6.9.2 with over 550 bug fixes, security updates, and other improvements done to Qt base. There are no additional Qt for Android Automotive features delivered with version 6.9.2.
We implemented Google’s Material 3 and Microsoft’s Fluent 2 design systems for QSkinny, as shown in the screenshots below (still an ongoing effort). This required some new features to be implemented, some of which hinted at new general paradigms within many other design systems.
Contrarily, some features often found in commercial design systems seem to have been deliberately left out of these two ‘generic’ ones above.
Google’s Material 3 design system
Microsoft’s Fluent 2 design system
Here is a subjective list of requirements for new design systems:
Of course the days of the 90s style thick drop shadows are gone, but apparently so is the complete flat style of the first iPhone. Shadows (and other ways of elevation) are used as a subtle way to draw attention and show levels of visual hierarchy.
For instance, a hovered or focused button will have a higher elevation than a resting button, and a dialog or a popup an even higher one. Typically these shadows have a minimal x/y offset, and a semitransparent color.
Material has a guide on elevation that lists shadows as one element, and colors as another one. Interestingly the usage of shadows in Material 3 was reduced in favor of coloring in comparison to its predecessor Material 2.
Fluent 2 similarly lists shadows and box strokes (i.e. box borders) as elements of elevation.
As a consequence, shadows should be 1st class citizens in UI toolkits: Defining a shadow on a control should be as easy as setting a color or a font.
Here is how QSkinny defines the gradient and shadow of the QskMenu class for Fluent 2:
using Q = QskMenu;
setBoxBorderColors( Q::Panel, pal.strokeColor.surface.flyout );
setGradient( Q::Panel, pal.background.flyout.defaultColor );
setShadowMetrics( Q::Panel, theme.shadow.flyout.metrics );
setShadowColor( Q::Panel, theme.shadow.flyout.color );
Fluent 2 menus in QSkinny
Side note: Inner shadows (as opposed to drop shadows) seem to be very rare and have only been observed in sophisticated controls for commercial design systems.
Material 3 comes with an incredibly sophisticated color/palette system (see below); but surfaces like button panels etc. consist of one color only (Sometimes two colors need to be “flattened” into one, e.g. a semi-transparent hover color and an opaque button panel color).
Same for Fluent 2: The design system lists several shades of its iconic blue color and more flavors of gray without any gradient.
However, many commercial design systems still use gradients for “2.5D-style” effects. Of course they are applied as subtly as the aforementioned shadows, but are still being used a lot.
The QSkinny example below shows two small buttons on the left with subtle linear gradients (45 degrees) in the inner panel and a reversed gradient on the outer button ring. This has both an elevation and a “texture” effect without actually using textures (i.e. images). The shadows add to the depth, but even with only gradients would the sense of elevation still be there.
On the right is a gauge with conical gradients: The inner green one going from dark green to lighter green, and the outer one trying to mimic a chromatic effect. Radial gradiants are not shown here, but are equally usable.
Gradients add depth and elevation
Outlook: For more sophistication there might be the need in the future of multi-dimensional gradients like the one below: a conical gradient (from purple to blue) and a radial one (transparent to full hue):
A gauge generated by AI showing multi-dimensional gradients
Google created a new color system called HCT (hue, chroma, tone) built on older ones like HSL (hue, saturation, lightness). The difference is that HCT is “perceptually accurate”, i.e. colors that feel lighter are actually recognized as lighter within HCT with a higher tone value.
By changing only the chroma (~ saturation) and tone (~ lightness) of a color, one can generate a whole palette to be used within a UI that matches the input color nicely; as an example, see the Material 3 palette on Figma.
In terms of accessibility, by choosing e.g. background and font colors with a big tone difference, one can be reasonably sure that text is comfortly legible.
So even developers without design experience can create a visibly appealing UI, without even knowing the main input color. Creating the palette depending on an input color is used on e.g. Android, where the user can choose a color or just upload an image, and all controls adapt to the main color.
Side note from the Qt world: QColor::darker() and QColor::lighter() are not fine-grained enough for this purpose.
With the additional support of changing colors dynamically (see below), this means that palettes can easily be generated for both day and night mode from one or two input colors; one could also easily change between different brand colors.
The QSkinny tractor example shows how to extend the Material 3 palette with more colors and how to change between day/night mode as well as different brands easily, as switching colors and other hints is supported directly in the framework.
QSkinny tractor example in day/night mode with “Ikea colors”
QSkinny tractor example in day/night mode with “John Deere colors”
For an online demo of this app, see the tractor example on the main page.
So while changing between branding themes as above might not be a day-to-day use case, changing between day and night mode certainly is. This means that a UI toolkit should be able to change skin colors dynamically.
Note: Ideally not only colors can be changed, but any type of skin definition like box roundings, fonts etc. QSkinny treats every definition token, or so called “skin hint”, the same and can therefore exchange any of those at runtime.
Graphics like the button icons and the bigger tractor icon in the example above should be flexible in several ways:
They should adapt to color changes, as they are typically part of a definition that might change with the current skin; i.e. if the font and background colors change, then so need the icons.
Equally important, they should be flexible in size. With the Material 3 skin, e.g. buttons can have a different size in pixels depending on the physical size of the screen used: On smartphones that have a high resolution and a low physical size, buttons tend to have a bigger size in pixels than on tablets or TVs. This means that also the icons might have to grow with the buttons; when using layouts, they might even have to grow dynamically at runtime when resizing a window.
For the use case of different physical sizes, Material 3 supports so-called “density-independent pixels”; Fluent 2 has a similar concept named “device-independent pixels”.
These two points of course hint at using raster graphics like SVGs rather than JPGs/PNGs for icons; at least in Qt they have made kind of a revival recently.
In QSkinny flexible graphics have been supported from the start, and fit nicely with the use of icons in Material 3 and Fluent 2.
Lastly, at some point those graphics might also need to be animated with e.g. lottie. Material 3 and Fluent 2 don’t seem to have an urgent need for this, but other design systems might have this requirement.
Implementing Material 3 and Fluent 2 for QSkinny was a painful but solid way to future-proof our UI toolkit.
Modern toolkits should be following the paradigms of modern design systems: They should allow for easy definition of shadows, or generally speaking: They should allow for styling all aspects of a “box” like gradients, border colors, box shapes, and shadows.
In addition they should allow for dynamically changing all aspects of the current style like colors, fonts and the aforementioned box aspects.
Talking in QSkinny terms, we believe that styling aspects of boxes and changing skin hints dynamically is well covered after having implemented bot Material 3 and Fluent 2.
Of course there is more work to do when it comes to more advanced features for other design systems like inner shadows, multi-dimensional gradients or multiple shadow colors.
Can you guess the “brand” from the colors being used in the screenshot below? Let me know and the first one to get it right will receive a drink of his/her choice next time we meet in person!
Which brand do these colors belong to? Hint: It is not really a company.
Qt for MCUs 2.8.3 LTS (Long Term Support) has been released and is available for download. This patch release provides bug fixes and other improvements while maintaining source compatibility with Qt for MCUs 2.8 (see Qt for MCUs 2.8 LTS released). This release does not add any new functionality.
For more information about fixed bugs, please check the Qt for MCUs 2.8.3 change log..
As with other Qt products, Qt for MCUs 2.8.3 LTS can be added to an existing installation by using the maintenance tool or via a clean installation using Qt Online Installer.
The standard support period for the Qt for MCUs 2.8 LTS ends in December 2025.
We are excited to announce the release of Qt 6.9.2! As a patch release, Qt 6.9.2 does not introduce new features but delivers over 550 bug fixes, security updates, and enhancements on top of the Qt 6.9.1 release. For a detailed breakdown of the most significant changes, refer to the Qt 6.9.2 release notes.
Qt's model/view framework was one of the big additions to Qt 4 in 2005, replacing the previous item-based list, table, and tree widgets with a more general abstraction. QAbstractItemModel
sits at the heart of this framework, and provides a virtual interface that allows implementers to make data available to the UI components. QAbstractItemModel
is part of the Qt Core module, and it is also the interface through which Qt Quick's item views read and write data.
The Qt Contributor Summit is an annual community event hosted by Qt Group, which brings together developers, contributors, and maintainers. This is a unique opportunity for participants to connect with each other, learn about the latest developments in Qt, and further contribute to the future of the framework.
AI is more than a tool for automation. When viewed as a partner, it can challenge us to become better developers, helping us see our own work with fresh eyes.
The post Squish Qt tips & tricks: Working with objects without unique properties appeared first on Spyrosoft.
If we think about it for a minute, it is impossible to imagine an app without a UI. We need buttons, checkboxes, dropdown menus and much more. Almost all of these controls are created using 2D elements. However, sometimes we need to put some depth into the UI.
In this blog you will learn how to set up clang-format with Qt Creator for consistent, automatic code formatting, including custom style files and exclusion rules for subdirectories.
Continue reading Using clang-format in Qt Creator at basysKom GmbH.
Hey, welcome back to another blog post. Today we’re going to talk about the new Qt WebAssembly. This post will […]
PySide6 is easy to use and powerful but there’s a few pitfalls to look out for. We’ll investigate a performance issue in a large data model, find out that function calls are expensive, and that some of Qt’s C++ APIs had to be adapted to work in a Python environment.
Learn how to use Cucumber-CPP and Gherkin to implement better black box tests for a C++ library. We developed a case-study based on Qt OPC UA.
Continue reading Improved Black Box Testing with Cucumber-CPP at basysKom GmbH.
The 6th edition of my book Create GUI Applications with Python & Qt is now available, for PyQt6 & PySide6.
This update brings the book up to date with the latest changes in PyQt6 & PySide6, and also updates code to make use of newer features in Python. Many of the chapters have been updated and extended with more examples of form layouts, built-in dialogs and architecture, particularly using Model View Controller (MVC) architecture.
You can buy the latest editions below --
As always, if you've previously bought a copy of the book you get these updates for free! Just go to your account downloads page and enter the email you used for the purchase.
If you bought the book elsewhere (in paperback or digital) you can register to get these updates too -- just email your receipt to register@pythonguis.com
Enjoy!
Technical managers in the embedded space often face a classic challenge: integrating industrial communication protocols into modern applications. One such […]
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.
Over the years, the capabilities of QtQuick's TableView have evolved dramatically-from early custom implementations to well supported feature in Qt 6.8 and newer. In this article we explore the progression of QtQuick/QML's TableView, outline the limitations of early versions, and highlight newer features such as custom selection modes, header synchronization, and lightweight editing delegates. Check it out.
Continue reading Modern TableView in QML: What’s New in Qt 6.8 and Beyond at basysKom GmbH.
Qt is a powerful tool and framework for building modern cross-platform applications. From embedded devices to desktop software, Qt enables […]
We cover Qt-related issues on our blog, but only recently realized that many of our readers may not even know […]
Qt and QML provide a powerful framework for developing modern, cross-platform applications with a sleek and responsive UI. While Qt […]
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.
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:
$ mkdir notes/
$ cd notes
$ python -m venv venv
$ source venv/bin/activate
(venv)$ pip install pyside6 sqlalchemy
> mkdir notes/
> cd notes
> python -m venv venv
> venv\Scripts\activate.bat
(venv)> pip install pyside6 sqlalchemy
$ 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.
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.
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.
python notes.py
When the applicaton launches you'll see the following on your 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.
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.
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 the QSS styling applied
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.
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
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.
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 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.
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.
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.
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
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
.
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
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.
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
.
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.
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
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.
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.
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()
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:
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.
engine = create_engine("sqlite:///notes.db")
Base.metadata.create_all(engine)
Save the database.py
file and run it
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
# Create a session to handle updates.
Session = sessionmaker(bind=engine)
session = Session()
The final complete code for our database interface is shown below
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.
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.
NoteWindow
must have it's own associated instance of the Note
object.
Note
objects should be created when creating a new NoteWindow
.
NoteWindow
should sync it's initial state to a Note
if provided.
NoteWindow
should update the data in the Note
.
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.
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.
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:
Note
object to session.delete
to remove it from the database,
active_notewindows
(so the object will be tidied up)
.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.
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.
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
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).
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:
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.
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:
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?
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!
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.
Step 1
Initial state, the email is in the inbox
Step 2
Dragging the email onto the Customers folder
Step 3
Dropping the email
Step 4
The email is now in the customers folder
Example code can be found here for flat models and here for tree models.
☑ 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
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;
}
☑ 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.
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
}
Example code:
☑ 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.
☑ 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:
This is a case where proper model/view separation is actually much simpler.
While writing and testing these code examples, I improved the following things in Qt, in addition to those listed in the previous blog posts:
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.
Standard support for Qt 5 will end in May. Time is running out to port your code!
Products from the STM32 family have been a popular target for embedded Qt applications for quite a long time. One […]