KD Chart 2.8.0 has been released!

KD Chart 2.8.0 has been released!

KD Chart is a comprehensive business charting package with many different chart types and a large number of customization options. We are constantly improving the package, and have been doing so for years.

KD Chart 2.8.0 is a very minor release. The most notable change is the removal of Qt 4 support. Additionally, users will receive a notification that the QMake build system will no longer be supported in KD Chart 3.0, to make way for CMake.

The next release of KD Chart will be 3.0.0, which will come with the ability to run KD Chart in a web browser. See https://demos.kdab.com/wasm/kdchart for an example.

For more information…

Find out more about KD Chart here.

The release highlights and version 2.8.0 itself can be found here.

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

KDAB provides homebrew formulas for Mac users, at https://github.com/KDAB/homebrew-tap.

Commercial users can access version 2.8.0 from their KDAB customer account or create a KDAB customer account here.

To discuss licensing options, contact us at info@kdab.com.

 

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

 

The post KD Chart 2.8.0 has been released! appeared first on KDAB.

XmlListModels in Qt 6

I had a look at a small XmlListModel based project of mine and started migrating the code from Qt 5.12 to Qt 6.2. The code ports pretty cleanly, but there are some caveats to be aware of.

As I’m lazy, I started by changing the imports from 2.12 to 6.2 and tried running the code. The first changes I had to make was to change the import from QtQuick.XmlListModel to QtQml.XmlListModel. I also learned that the import statement no longer requires a specific version to be specified – I’m not sure if I’m a fan of that quite yet.

The second change was that XmlRole has been renamed to XmlListModelRole, and that it no longer has a query property, but an elementName and attributeName property. I guess that saves Qt from having to implement support for XPath queries, and in my use-case (and most others), this should still be enough.

The last change I had to made was to silence a warning. It is no longer encouraged to connect objects directly to signals in QML. In my case, it was animations triggered by the onAdd and onRemove signals in a model. The trick is to declare the animation (in my case, a pair of SequentialAnimation instances, separately. Provide an id for them, and then call start on that id in the signal handler.

All in all, a quite pleasant migration experience with only superficial API changes to handle. All logic could be used as is. Nice!

Qt Extras Modules in Qt 6

Qt 6 is a result of the conscious effort to make the framework more efficient and easy to use.

We try to maintain binary and source compatibility for all the public APIs in each release, but some changes were inevitable in an effort to make Qt a better framework. One of those changes was to remove the platform-specific Extras modules, to ensure a cohesive cross-platform story and future for Qt 6.

Q&A: How can I enable editing on a QTableView? — Modifying your model to allow editing of your data source

In the model views course we covered Displaying tabular data in Qt5 ModelViews. This takes a data source, for example a list of list objects, a numpy array or a Pandas DataTable and displays it in a Qt table view. But often, displaying is just the first step -- you also want your users to be able to add and edit the table, updating the underlying data object.

Reader Vic T asked:

I have been trying for a few days to get edit mode to work with a QTableView using Pandas for the model via QAbstractTableModel. Having searched all over the internet although I found suggestions to implement the flags() method but it doesn’t seem to work.

This is correct -- you need to implement the .flags() method on your model to inform Qt that your model supports editing. To do this your method needs to return the Qt.ItemIsEditable flag, which you or together (using the pipe | character) with the other flags. For example

python
    def flags(self, index):
        return Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsEditable

But to get the editing working you also need to implement a .setData method. This is the model's interface between Qt and your data object and takes care of making the changes to the data.

Remember, Qt model views don't know anything about your data beyond what you tell them via the model. Likewise, they also don't know how to update your list, array or DataFrame objects with the new data that has been input. You need to handle that yourself!

Below are some example .setData methods for list of list data structures, numpy and pandas. The only difference is how we index into the data object.

python
    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data[index.row()][index.column()] = value
            return True

python
    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data[index.row(), index.column()] = value
            return True

python
    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data.iloc[index.row(),index.column()] = value
            return True

Notice that we first need to check the role is Qt.EditRole to determine if an edit is currently being made. After making the edit, we return True to confirm this.

If you try the above on your model, you should be able to edit the values. However, you'll notice that when editing it clears the current value of the cell -- you have to start from an empty cell. To display the current value when editing you need to modify the .data method to return the current value when the role is Qt.EditRole as well as when it is Qt.DisplayRole. For example:

python
    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row()][index.column()]
                return str(value)

python
    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row(), index.column()]
                return str(value)

python
    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data.iloc[index.row(), index.column()]
                return str(value)

That's it, you should now have a properly editable table view.

Below are some complete working examples for list data, numpy and Pandas tables, with PyQt5, PyQt6, PySide2 & PySide6

list of list

The following examples use a nested list of lists as a data source.

python
import sys

from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row()][index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data[index.row()][index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = [
            [1, 9, 2],
            [1, 0, -1],
            [3, 5, 2],
            [3, 3, 2],
            [5, 8, 9],
        ]

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python
import sys

from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
                value = self._data[index.row()][index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.ItemDataRole.EditRole:
            self._data[index.row()][index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable


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

        self.table = QTableView()

        data = [
            [1, 9, 2],
            [1, 0, -1],
            [3, 5, 2],
            [3, 3, 2],
            [5, 8, 9],
        ]

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python
import sys

from PySide2.QtCore import QAbstractTableModel, Qt
from PySide2.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row()][index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data[index.row()][index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = [
            [1, 9, 2],
            [1, 0, -1],
            [3, 5, 2],
            [3, 3, 2],
            [5, 8, 9],
        ]

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python
import sys

from PySide6.QtCore import QAbstractTableModel, Qt
from PySide6.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        # The length of the outer list.
        return len(self._data)

    def columnCount(self, index):
        # The following takes the first sub-list, and returns
        # the length (only works if all rows are an equal length)
        return len(self._data[0])

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row()][index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data[index.row()][index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = [
            [1, 9, 2],
            [1, 0, -1],
            [3, 5, 2],
            [3, 3, 2],
            [5, 8, 9],
        ]

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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

Pandas

The following examples use a Pandas DataFrame, adding column headings from the column names.

python
import sys

import pandas as pd
from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, parnet=None):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data.iloc[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data.iloc[index.row(), index.column()] = value
            return True
        return False

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[col]

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = pd.DataFrame([[1, 9, 2], [1, 0, -1], [3, 5, 2], [3, 3, 2], [5, 8, 9],], columns=["A", "B", "C"])

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python

import sys

import pandas as pd
from PyQt6.QtCore import QAbstractTableModel, Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, parnet=None):
        return self._data.shape[1]

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if index.isValid():
            if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
                value = self._data.iloc[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.ItemDataRole.EditRole:
            self._data.iloc[index.row(), index.column()] = value
            return True
        return False

    def headerData(self, col, orientation, role):
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self._data.columns[col]

    def flags(self, index):
        return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable


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

        self.table = QTableView()

        data = pd.DataFrame(
            [[1, 9, 2], [1, 0, -1], [3, 5, 2], [3, 3, 2], [5, 8, 9],], columns=["A", "B", "C"]
        )

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python

import sys

import pandas as pd
from PySide2.QtCore import QAbstractTableModel, Qt
from PySide2.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, parnet=None):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data.iloc[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data.iloc[index.row(), index.column()] = value
            return True
        return False

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[col]

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = pd.DataFrame(
            [[1, 9, 2], [1, 0, -1], [3, 5, 2], [3, 3, 2], [5, 8, 9],], columns=["A", "B", "C"]
        )

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python

import sys

import pandas as pd
from PySide6.QtCore import QAbstractTableModel, Qt
from PySide6.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, parnet=None):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data.iloc[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            self._data.iloc[index.row(), index.column()] = value
            return True
        return False

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._data.columns[col]

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = pd.DataFrame(
            [[1, 9, 2], [1, 0, -1], [3, 5, 2], [3, 3, 2], [5, 8, 9],], columns=["A", "B", "C"]
        )

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


Numpy

The following examples use a numpy array for their data source. The array will only accept valid values (in this case integers) when setting, so we must first coerce the value to an integer before setting it on the error. If you enter something which isn't a valid integer (e.g. jdskfjdskjfndsf ) the int() call will throw a ValueError, which we catch. By returning False when this exception is thrown we cancel the edit.

python

import sys

import numpy as np
from PyQt5.QtCore import QAbstractTableModel, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            try:
                value = int(value)
            except ValueError:
                return False
            self._data[index.row(), index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = np.array([
          [1, 9, 2],
          [1, 0, -1],
          [3, 5, 2],
          [3, 3, 2],
          [5, 8, 9],
        ])

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python

import sys

import numpy as np
from PyQt6.QtCore import QAbstractTableModel, Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if index.isValid():
            if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
                value = self._data[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            try:
                value = int(value)
            except ValueError:
                return False
            self._data[index.row(), index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable


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

        self.table = QTableView()

        data = np.array([
          [1, 9, 2],
          [1, 0, -1],
          [3, 5, 2],
          [3, 3, 2],
          [5, 8, 9],
        ])

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python

import sys

import numpy as np
from PySide2.QtCore import QAbstractTableModel, Qt
from PySide2.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            try:
                value = int(value)
            except ValueError:
                return False
            self._data[index.row(), index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = np.array([
          [1, 9, 2],
          [1, 0, -1],
          [3, 5, 2],
          [3, 3, 2],
          [5, 8, 9],
        ])

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


python

import sys

import numpy as np
from PySide6.QtCore import QAbstractTableModel, Qt
from PySide6.QtWidgets import QApplication, QMainWindow, QTableView


class PandasModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        self._data = data

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def data(self, index, role=Qt.DisplayRole):
        if index.isValid():
            if role == Qt.DisplayRole or role == Qt.EditRole:
                value = self._data[index.row(), index.column()]
                return str(value)

    def setData(self, index, value, role):
        if role == Qt.EditRole:
            try:
                value = int(value)
            except ValueError:
                return False
            self._data[index.row(), index.column()] = value
            return True
        return False

    def flags(self, index):
        return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable


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

        self.table = QTableView()

        data = np.array([
          [1, 9, 2],
          [1, 0, -1],
          [3, 5, 2],
          [3, 3, 2],
          [5, 8, 9],
        ])

        self.model = PandasModel(data)
        self.table.setModel(self.model)

        self.setCentralWidget(self.table)


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


That's all for now!

For an in-depth guide to building Python GUIs with PySide2 see my book, Create GUI Applications with Python & Qt5.

GitQlient 1.4.0 released

I’m happy to announce the release of GitQlient 1.4.0

Five months after the last big release, I present you a new GitQlient 1.4.0 version. This comes with less features than the previous versions but in exchange I hope it brings better performance.

In this release I’ve focused in the performance of the Git queries and the RAM memory used by GitQlient. I’ve done a deep research on the usage of QString and the allocations that were done to reduce them to the minimum. I’ve also added an internal cache system so now the UI won’t freeze every time the main graph needs to be refreshed. This also includes the separation between the data update of the references and the log history.

During this version I’ve paused the development of GitHub integration although is totally stable and usable, and the integration of Jenkins. This last integration will take a bit longer to refactor so I’ll focus on it in the following releases.

GitQlient 1.4.0 binaries

You can find the binaries for GitQlient 1.4.0 on the release section on the GitHub repo:

New features in GitQlient 1.4.0?

  • Squash merge branches
  • Squash commits: it is now possible to squash the last commits of the current branch. Of course, if the commits where already pushed to remote, they will need to be pushed force.
  • GitQlient now has an internal cache: Local Git operations will be done against the cache and later asynchronously against remote. This allow GitQlient to run faster for local changes that doesn’t need synchronization. This also reduces the UI freeze when refreshing because of changes in the graph.
  • Credentials can be managed by GitQlient with the proper configuration. This can be done through the Config screen.
  • Multi cherry-pick: It is possible now to cherry-pick several commits at once when they’re not part of the current branch.
  • Search tags in the Branches widget: In addition to branches, it is possible to search tags by name.
  • DEB package: GitQlient is now release as well as a DEB package in addition to RPM.

Other features

Technical improvements:

  • Reduced the number of memory allocations (specially with QString)

Known issues:

  • Any MacOS issues are still opened waiting for a system to be tested on.

GitQlient 1.4.0 released first appeared on My C++ & Qt blog - Cesc M..

Qt WebAssembly: prompt on exit

 I sometimes get asked if there is a way to let a Qt app ask the user if they really want to close the app when the browser tab or window gets closed by the user.


The answer is yes and no. Yes, in that there is, and no in that it won't be your own Qt dialog/prompt.


We can use javascript in Qt webassembly apps, so the prompt will be opened by the browsers javascript. 


How?

There are two ways to run javascript. By using emscripten macro EM_ASM, or including emscripten/val.h and using straight c++.


EM_ASM:

The is the easiest way but uses a macro.


#include <emscripten.h>

Just add something like this, in your app constructor function:


    EM_ASM(
           window.onbeforeunload = function (e) {
                e = e || window.event;
                return 'Sure';
            };
    );


C++:

This is a bit more complicated, and involves a global static function.

#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <emscripten.h>


static emscripten::val mybrowserBeforeUnload(emscripten::val e)
{
   return emscripten::val("Sure");
}


EMSCRIPTEN_BINDINGS(app) // must be unique name!
{
    function("myBrowserBeforeUnload", &mybrowserBeforeUnload);
}


and in the c'tor:

 emscripten::val::global("window").set("onbeforeunload", emscripten::val::module_property("myBrowserBeforeUnload"));



The key is to return a string. Any string will do.
and it looks something like this:





How tu customise installer behaviour with Qt Installer Framework

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

 

API overview

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

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

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

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

Controller script basics

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

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

function Controller()
{
}

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

function Controller()
{
}

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

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

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

<ControlScript>script.qs</ControlScript>

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

Component script basics

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

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

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

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

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

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

Adding a shortcut 

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

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

function Component()
{
    // default constructor
}

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

Recursive installation 

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

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

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

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

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

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

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

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

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

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

Summary 

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

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

Qt 6.2 Beta Released

I am pleased to announce that we released the first Qt 6.2 Beta today. Qt 6.2 includes all widely used Qt add-on modules and is also the first release in the Qt 6 series to provide Long Term Support for commercial licensees. We will continue to provide subsequent beta releases via the online installer throughout the beta phase.

KDSoap 2.0.0 Released

KDSoap 2.0.0 was just released!

What is KDSoap?

KDSoap is a tool for creating client applications for web services, without the need for any further component such as a dedicated web server. This tool makes it possible to interact with applications which have APIs that can be exported as SOAP objects. The web service then provides a machine-accessible interface to its functionality via HTTP.

KDSoap also supports writing SOAP servers easily, using Qt code.

About 2.0.0

KDSoap 2.0.0 is our first release that supports Qt 6, in addition to Qt 5. Also, the qmake buildsystem is fully removed in favor of CMake.

Other fixes and improvements that come with v2.0.0 include an uninstall target in the buildsystem, generation+installation of the API manual in qch format for Qt assistant, and the addition of options to the KDSoapClient class specifying the SOAP action sending method.

Need more information?

Here is a list of release highlights:

General:

  • Supports Qt6 in addition to Qt5
  • Minimum Qt version is 5.9
  • The qmake buildsystem (via autogen.py) is removed in favor of CMake
  • buildsystem: a new ‘docs’ target is created when CMake -DKDSoap_DOCS=True.
  • buildsystem: the API manual is now generated in build/docs vice source/docs.
  • buildsystem: added an uninstall target
  • buildsystem: generate and install kdsoap-version.h
  • The API manual is generated+installed in qch format for Qt assistant.

Client-side:

  • Added options to the KDSoapClient class specifying the SOAP action sending method

WSDL parser / code generator changes, applying to both client and server side:

  • Fix generated code in case a variable is called “d” or “q”
  • Fix generated code for an enumeration type with a length restriction
  • Avoid potential type collisions in nested complexTypes

 

What’s next?

Get KDSoap version 2.0.0.

Learn more about KDSoap.


About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post KDSoap 2.0.0 Released appeared first on KDAB.

Qt for MCUs 1.9 released

A new feature update of Qt for MCUs is now available. Download version 1.9 to create your applications on Linux systems and for new target platforms, get access to the new PaintedItem and font quality APIs, discover new demos and examples, and more.

Qt 6 Reaches Feature Parity with Qt 5 - the Qt 6.2 Alpha Released

We have released Qt 6.2 Alpha today. The Qt 6.2 is the first Qt 6 release which includes all widely used Qt add-on modules previously available in the Qt 5.15 release. Compared to Qt 6.1 the upcoming Qt 6.2 brings support to 13 additional modules totaling to over 50 modules supported with Qt 6.2. On top of this, Qt 6.2 is also the first release in the Qt 6 series to provide Long Term Support for commercial licensees.

On the Removal of toSet(), toList() and Others

(Apologies for the clickbait in the post title! But I’d really like people who are searching for solutions to read this.)

Between Qt 5.14 and Qt 5.15, my colleague, Marc Mutz, and I submitted a series of patches to Qt that added “range constructors” to the Qt containers. This brought Qt containers one step closer to…C++98 feature parity! 🙂

What’s a range constructor? It’s a constructor that takes an iterator pair (begin, end) and constructs a container by reading the corresponding range of elements:

// Creates a QList from the [begin, end) range
QList<int> intList( begin, end ); 

Usually the range is obtained from another container of a different type. This allows for converting a container to another one, for instance, converting a QList to a QSet:

QList<int> intList = ~~~;

// Creates a QSet from a QList
QSet<int> intSet( intList.begin(), intList.end() ); 

This approach works no matter what is the source container: it can be a QList, a QVector, a std::deque, and what not.

Some Qt containers also provided a similar functionality. For instance, in Qt 5, QList had the toSet() conversion function that returns a QSet with the same elements:

QList<int> intList = ~~~;

// Creates a QSet from a QList (in Qt 5)
QSet<int> intSet = intList.toSet();

The new constructors are a generalization of these conversion functions. They work with any source container, including the ones from the Standard Library, but also those from Qt that were simply lacking the conversion (did you ever notice that there isn’t a QVector::toSet()?).

Therefore, in a related set of changes, the conversion functions have been deprecated in Qt 5.15 and then removed in Qt 6.0.

This has made a lot of people very angry and been widely regarded as a bad move.

Wait, that’s another thing…

Anyway, on the web, you may find quite a bit of unhappiness about the change. There’s a Qt bug report that tries to collect this information, as well as some threads on the Qt mailing lists.

The common factor to the complaints was that the deprecation and subsequent removal looked entirely gratuitous. In other words, is it only because we don’t want “duplication” in the APIs that we are asking users to replace perfectly working code with code that is:

  • functionally identical (does the same thing),
  • behaviorally identical (it does the same thing in the same way),
  • and much more verbose and awkward to use (list.toSet() vs QSet(list.begin(), list.end()))?

That sounds silly at best; and certainly against Qt policies of offering nice-to-use APIs.

That’s why we…actually have nice things!

Qt has always had convenience methods on its containers. This is perfectly natural in Qt:

QList<int> intList = getAnswers();

if (intList.contains(42))
   std::cout << "The Answer has been found!\n";

Only C++23 will perhaps add something as convenient to the Standard Library:

std::vector<int> intVector = getAnswers();

if (std::ranges::contains(intVector, 42)) // if P2302 gets adopted
   std::cout << "The Answer has been found!\n";

In the meanwhile, this is the “common” way we do it today:

std::vector<int> intVector = ~~~;

if (std::find(intVector.begin(), intVector.end(), 42) != intVector.end())
   std::cout << "The Answer has been found!\n";

Quite a mouthful when compared to Qt’s way, right?

So if we have standard algorithms (like std::find), why do Qt containers still offer contains, indexOf, and so on? Should we get rid of them, too?

The answer, generally speaking, is no. Qt does not strive for minimality, it strives for convenience. And having those functions is convenient for end users.

The struggle between “easy to use,” and “hard to misuse”

Convenience must never be a trade-off for correctness. This is a given.

But convenience should also not let people misuse the APIs for the intended usage. Unfortunately, this is much harder to enforce when simply providing a correct API.

Case in point: toSet() has been introduced as a conversion function from QList to QSet. It does what it says, in the best way possible.

But how and where is toSet() actually used? Do people really need to convert lists to sets all the time?

Turns out that no, they don’t! Most of the time, the conversion has been an API abuse. For instance, the conversion has been used to remove duplicates from a QList:

QList<int> list = ~~~;

// Remove duplicates, and get back to the original list
// For the love of kittens, DON'T DO THIS!
list = list.toSet().toList();

Or similarly, it has been used to process elements uniquely:

QList<int> list = ~~~;

// Process elements, skipping duplicates. 
// This is OK-ish, but there are better solutions!
const QSet<int> set = list.toSet();
for (int i : set)
  process(i);

Or to generate an list of unique elements when joining multiple lists:

QSet<int> temp;
for (int i = 0; i < N; ++i) {
  temp += getList(i).toSet();
}
QList<int> result = temp.toList();

All of these usages have a common denominator: they abuse the container API instead of using an algorithm. There’s a better, algorithmic solution for most of these. To put in another way, very few of these really require the “convenience” provided by toSet(), and the few ones that do need it are easy to port away to the range constructors.

So why do people immediately jump to using the converting functions between containers?

As a C++ teacher, it’s a hard truth for me to digest. Here, I mostly see a failure at educating C++ developers.

Developers using toSet() have no faults. We teach them about containers. We tell them that QList is sequential, random access; that QSet has a random order and doesn’t allow for duplicates; and so on. We don’t teach them about algorithms, or at least not together with containers; we don’t teach about their importance, or we just don’t teach about them at all.

So, when faced with a very concrete problem, such as how do I remove duplicates from a QList?, developers immediately jump to QSet. Hey! It’s the thing that doesn’t allow duplicates in the first place! “So how do I go from a QList to a QSet? Oh, there’s converting function, toSet(). Job done; move on.”

The correct mental approach of getting rid of duplicates should be different. That is, the default go-to for an algorithmic problem should be an algorithm. Just searching on Google would reveal the correct algorithm to use.

Please note, the fundamental reason why I’d love to spread more knowledge about algorithms isn’t just about performance. Yes, removing duplicates via QSet will cost you an awful lot more when compared with a std::sort+std::unique approach:

  • CPU: hashing, allocating, copying elements around, destroying everything/deallocating
  • Memory: at least #(number of unique elements) memory allocations (remember, in Qt 5, QSet is implemented as a bucket of chains)

But I don’t want to go there. I’m sure that someone will come up with some specific numbers and datatypes for which the QSet approach will actually be faster. In the bigger scale, it’s the wrong approach, and we shouldn’t let users choose the wrong approach. They deserve better. We’re not doing them a favor by supporting APIs such as toSet() and not teaching them the alternatives.

That’s the real reason for deprecating toSet(). It’s an API which is too easy to misuse, and correctly serves just very few users.

Summary (or: TL;DR: give me the solutions!)

Here are a few quick recipes for going forward.

“I need to eliminate duplicates from a linear, sequential container (QList, QVector, std::vector, etc.).”

If you can reorder the elements in the container itself, then use an algorithm (technically, two: sort and unique; plus a call to the container’s erase), it is the fastest option available, at zero extra memory cost.

You can package this as a utility function:

std::sort(container.begin(), container.end());
container.erase(std::unique(container.begin(), container.end()),
                container.end()); 

If you cannot reorder the elements in the container, then use a different algorithm!

KDToolBox::DuplicateTracker<T> tracker;
const auto isDuplicate = [&tracker](const T &t) { return tracker.hasSeen(t); };
container.erase(std::remove_if(container.begin(), container.end(), isDuplicate),
                container.end());

KDToolBox::DuplicateTracker is a small utility class from KDToolBox, KDAB’s collection of miscellaneous useful C++ classes and stuff. It wraps a set and also uses monotonic_buffer_resource to improve the memory usage for small containers, avoiding allocations in those scenarios.

In either case, you may want to also call container.shrink_to_fit(); at the end, if you want to shed extra capacity.

“I need to process unique elements, skipping duplicates.”

Use KDToolBox::DuplicateTracker again:

KDToolBox::DuplicateTracker<T> tracker;
for (const T &t : container) {
  if (!tracker.hasSeen(t))
    process(t);
}

Of course, bear in mind that this may cost you the same size of container (if it just contains unique elements).

“I really, really need to convert a QList (or another container) to a QSet.”

You can use the ranged constructor:

QSet<int> set(list.begin(), list.end());

If you obtain the list from a function call, however, be sure NOT to call the function twice!

QList<int> getList();

QSet<int> set(getList().begin(), getList().end()); // !!! WRONG !!!

Instead, do this:

QList<int> getList();

const QList<int> list = getList();
QSet<int> set(list.begin(), list.end()); // OK

The double call is expensive and, also, wrong (you’re getting potentially two distinct containers back).

“It’s still me. I really, really need to convert a QList (or another container) to a QSet. AND I HATE THIS SYNTAX!”

If you can, add range-v3 to your project, and you will be able to do even better:

QSet<int> set1 = list | ranges::to<QSet>();
QSet<int> set2 = getList() | ranges::to<QSet>();

This is pretty sweet to type.

“It’s still me. I really, really need to convert a QList (or another container) to a QSet. range-v3 sounds sweet, but I can’t use it yet — slows down my compilation times too much, etc.”

Then use KDToolBox again. It features a minimal implementation of ranges::to:

QSet<int> set1 = list | kdToContainer<QSet>();
QSet<int> set2 = getList() | kdToContainer<QSet>();

FAQs

Could Qt have left these methods in Qt 6, although deprecated?

That is a very good question. I personally don’t have a good answer. The point of deprecated functionality is that, sooner or later, it will need to be removed, and Qt 6 looked like the perfect time to do so.

Why not add some generic Container::to<OtherContainer>() functions?

As I said, converting between containers is, in and of itself, a sketchy operation. There’s a way to do so; why should we support them with extra convenience? Does the potential for good usage outweigh the potential for misusage? The data so far shows that no, it doesn’t. But please, don’t let me discourage you from contributing these functions to Qt!

Thank you for reading, and I’ll see you next time.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post On the Removal of toSet(), toList() and Others appeared first on KDAB.

Animating custom widgets with QPropertyAnimation — Add dynamic visual effects to your custom widgets

In the previous tutorial we looked at how you can build custom widgets with PySide. The widget we built used a combination of layouts, nested widgets and a simple QPainter canvas to create a customized widget you can drop into any application.

But that just scratched the surface of what is possible with custom widgets in PySide. In this tutorial we'll look at how you can use Qt's QPropertyAnimation to enhance your custom widgets with visual effects and animations.

Window with two animated checkbox toggles The custom animated toggle checkbox in action.

QPropertyAnimation allows you to change the value of an attribute of an object from a startValue to a endValue over a certain amount of time, and optionally following a custom easingCurve.

To do this the attribute you want to change must be defined as a Qt property. Before moving forward with QPropertyAnimation let's take a look at the property concept in Python & Qt.

Properties

Objects in Python have attributes which you can set and get values from. These can be defined on the class itself (making them class attributes) or on the object instance (making them object attributes). Default values for object attributes are usually set in the __init__ method of the class, by assigning to self.<attribute name>.

python
class MyObject:

    def __init__(self):
        self.my_attribute = 1
        self.my_attribute2 = 2

obj = MyObject()
print(obj.my_attribute)
print(obj.my_attribute2)
obj.my_attribute = 'hello'
print(obj.my_attribute)

If you run this example, you'll see the following output.

python
1
2
hello

When we create an instance of the MyObject class, the __init__ method sets the two attributes my_attribute and my_attribute2. We can read these by accessing from the instance with obj.my_attribute, or set them by assigning to the attribute with obj.my_attribute = <value>.

While simple attributes are great for most use-cases, sometimes it is useful to be able to perform additional steps when getting and setting a value. For example, perhaps you want to send notifications in response to a change, or perform some kind of calculation on values as they are set. In these cases, you can use properties.

Python properties

Python properties behave outwardly exactly like attributes -- you can set and get them just as you would a normal attribute. Internally however each property uses getter and (optionally) setter methods to handle the get and set operations respectively.

The getter and setter methods are separate from one another. The getter is mandatory. If you don't define a setter, the property is read only.

You define properties using the property built-in. You can use this to define a property in two ways --

  1. Using property() as a function.
  2. Using @property as a decorator1

The following example shows how to define custom properties on a simple class using both these approaches.

python
class MyCustomClass:

    def __init__(self):
        self._value = None

    @property
    def value(self):
        print("getting the value", self._value)
        return self._value

    @value.setter
    def value(self, value):
        print("setting the value", value)
        self._value = value


obj = MyCustomClass()

a = obj.value       # Access the value
print(a)            # Print the value
obj.value = 'hello' # Set the value
b = obj.value       # Access the value
print(b)            # Print the value

python
class MyCustomClass:

    def __init__(self):
        self._value = None

    def getValue(self):
        print("getting the value", self._value)
        return self._value

    def setValue(self, value):
        print("setting the value", value)
        self._value = value

    value = property(getValue, setValue)


obj = MyCustomClass()

a = obj.value       # Access the value
print(a)            # Print the value
obj.value = 'hello' # Set the value
b = obj.value       # Access the value
print(b)            # Print the value

I prefer the @decorator syntax, since it keeps the method names the same as the value that you are setting/getting through the property -- but which you choose is up to you. If you run either example, you'll see the same output.

python
getting the value None
None
setting the value hello
getting the value hello
hello

When we access the obj.value property the @property decorated value method is run, printing the "getting the value" message. The value is returned as for any other object attribute. When we set the value, the @value.setter decorated method is run, printing the "setting the value" message.

The actual value is stored internally in a private attribute self._value which we provide with a default value in the object __init__.

Qt Properties

Qt properties work in a similar way, allowing us to define attributes on Qt classes and implement getter and setter methods to perform other functions. However, defining Qt properties also allows us to integrate with other Qt components.

To define a property in PySide we use Property which is importable from the QtCore module. As with Python properties, both can be used either as a function or as a decorator.

The only difference with the Python approach, is that for Qt we must also provide a type for the property -- in the example below int -- so Qt knows what type of data it can receive from/send to this property.

python
from PySide2.QtCore import Property

class CustomObject(QObject):
    def __init__(self):
        super().__init__()
        self._value = 0        # the default value

    @Property(int)
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = value

python
from PySide2.QtCore import Property

class CustomObject(QObject):
    def __init__(self):
        super().__init__()
        self._value = 0        # the default value

    def getValue(self):
        return self._value

    def setValue(self, value):
        self._value = value

    value = Property(int, getValue, setValue)

As before, if we create an instance of this class, we can now get and set its value member as if it is a normal attribute, e.g. --

python
obj = CustomObject()
obj.value = 7
print(obj.value)

One simple use for getter/setter methods in PySide application is to emit signals when certain attributes are changed. For example, in the below snippet we've added a custom Signal to the class, and emit the new value whenever the value changed.

python
from PySide2.QtCore import Property, Signal

class CustomObject(QObject):

    valueChanged = Signal(int)

    def __init__(self):
        super().__init__()
        self._value = 0        # the default value

    # change the setter function to be as:
    @value.setter
    def value(self, value):
        # here, the check is very important..
        # to prevent unneeded signal being propagated.
        if value != self._value:
            self._value = value
            self.valueChanged.emit(value)

python
from PySide6.QtCore import Property, Signal

class CustomObject(QObject):

    valueChanged = Signal(int)

    def __init__(self):
        super().__init__()
        self._value = 0        # the default value

    # change the setter function to be as:
    @value.setter
    def value(self, value):
        # here, the check is very important..
        # to prevent unneeded signal being propagated.
        if value != self._value:
            self._value = value
            self.valueChanged.emit(value)

Now we're familiar with using properties in PySide (and Python) we'll now look at how we can use QPropertyAnimation to animate properties and use this to create custom widget animations.

QPropertyAnimation

So far we've defined simple properties with setter and getter methods that behave like simple attributes. We also added a side-effect to a setter method to emit a signal notifying of a change.

QPropertyAnimation is an interface built upon properties which can be used to animate -- or interpolate -- between start and end values for a given property. Using this we can trigger a change and have a series of timed values set automatically.

If altering this property triggers an refresh of the widget (or we use the animated value in a paintEvent()) the widget will appear to animate.

Below is an example using QPropertyAnimation to animate the position of a simple QWidget -- a red filled square -- in a window. The animation updates the position of the widget via .pos which automatically triggers a repaint by Qt.

python
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import QPropertyAnimation, QPoint, QEasingCurve

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEndValue(QPoint(400, 400))
        self.anim.setDuration(1500)
        self.anim.start()

python
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import QPropertyAnimation, QPoint, QEasingCurve

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEndValue(QPoint(400, 400))
        self.anim.setDuration(1500)
        self.anim.start()

This will produce the following animation. By default the animation is linear, with the QWidget moving towards the end position at a constant rate.

Linear Single linear animation of a widget.

To create an animation using QPropertyAnimation you need to provide the following --

  1. tell the QPropertyAnimation which object we want to animate, here self.child
  2. Provide a property name here b"pos" (must be specified as bytes b"value")
  3. [Optional] the start value.
  4. The end value.
  5. [Optional] the duration of interpolation [in ms], by default it's 250 ms.

The property you're animating must have a setter -- default properties on built-in widgets have setters, but for custom widgets you need to implement this.

Rather than a simple linear animation you often want to add acceleration and deacceleration to the animation. This can be useful for creating widgets that feel realistic and physical, or to add interesting eye-catching effects. To add acceleration and deacceleration to an animation you use easing curves via QEasingCurve.

QEasingCurve

QEasingCurve is a Qt object which describes the transition -- or interpolation -- between two points. We can apply this transition to our animated properties to change how they behave.

In physical objects changes rarely happen at constant speed, but rather have a acceleration and deaccelerate phase. For example the speed of a falling object will start slow and increase over time due to gravity. A kicked ball accelerate rapidly -- but not instantly -- to full speed, and then deaccelerate due to air resistance. If you move an object with your hand you will accelerate it gradually before deaccelerating as you reach the destination for accuracy.

Try it in real life! Just grab any thing nearby and watch how your hand moves.

When moving a GUI component it can look unnatural if it has constant speed. To allow us to define more natural looking behaviors, Qt provides us with several common predefined curves.

The graphs may seem a little strange if you are not familiar with transition curves, so we'll look at them in a little more detail.

Each curve represents a value vs. time relationship, i.e. they show how a value will change over time. If the line is rising, the value is increasing, if the line is falling the value is decreasing. The slope or gradient of the line at any given point represents the rate of change (how quickly the value is changing) at that particular point. Steeper slopes indicate faster changes, while a horizontal line indicates the value is not changing, or constant, at that point.

Common curves

The default "curve" is not a curve at all, but a line. This Linear easing curve interpolates between two values in regular, consistent steps.

QEasingCurve.Linear QEasingCurve.Linear

Next is one of the most common transition curves used in UIs -- InOutCubic. This starts with a low slope, which increases until the mid-point, before decreasing again. The effect of this curve a gradual change, which accelerates to the mid-point, before de-accelerating to a stop at the end.

QEasingCurve.InOutCubic QEasingCurve.InOutCubic

There are also variants that only apply this transition in one direction (in, or out).

QEasingCurve.InCubic QEasingCurve.InCubic

QEasingCurve.OutCubic QEasingCurve.OutCubic

The OutInCubic is the reverse of InOutCubic and accelerates rapidly at the beginning, slowing down at the midpoint, then accelerates towards the end. This may be useful for slideshows or infinitely moving and changing components, where you want elements to rush into view and then pause before exiting.

QEasingCurve.OutInCubic QEasingCurve.OutInCubic

Last one OutBounce, show funny out of box animation, give a look on its animated demo below.

QEasingCurve.InBounce QEasingCurve.InBounce

QEasingCurve.OutBounce QEasingCurve.OutBounce

It's easier to understand these transitions if you see them in action. Below are a series of complete examples which you can experiment with, and adapt to the other transitions.

python
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import QPropertyAnimation, QPoint, QEasingCurve

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEasingCurve(QEasingCurve.InOutCubic)
        self.anim.setEndValue(QPoint(400, 400))
        self.anim.setDuration(1500)
        self.anim.start()

python
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import QPropertyAnimation, QPoint, QEasingCurve

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEasingCurve(QEasingCurve.OutInCubic)
        self.anim.setEndValue(QPoint(400, 400))
        self.anim.setDuration(1500)
        self.anim.start()

python
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import QPropertyAnimation, QPoint, QEasingCurve

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEasingCurve(QEasingCurve.OutBounce)
        self.anim.setEndValue(QPoint(400, 400))
        self.anim.setDuration(1500)
        self.anim.start()

InOutCubic Single InOutCubic animation of a widget.

OutInCubic Single OutInCubic animation of a widget.

OutBounce Single OutBounce animation of a widget.

The timing of each of these animations is identical (1.5 seconds) the difference in the animations is due to recording. In a Qt app each animation will take exactly the same time.

We've only looked at the most common of the easing curves. For the complete list refer to the Qt QEasingCurve documentation and experiment! You will find several charts in the documentation to visualize their behavior.

Combining Multiple QPropertyAnimation animations

These single animation curves are useful on their own, but sometimes you may want to combine multiple animations together to build more complex behaviors. To support this, Qt provides QAnimationGroup, with which we can combine several animations and control when they start and stop. There are two classes of animation group, which group animations in specific ways --

  • QParallelAnimationGroup groups animations to run at the same time
  • QSequentialAnimationGroup groups animations to run sequentially in order

QAnimationGroup is an abstract class, so can't be used directly.

Below is an example moving a widget with two sequential animations. The first moves the block as before, the second expands the size of the block horizontally.

python
from PySide2.QtWidgets import QWidget
from PySide2.QtCore import (
    QPropertyAnimation, QSequentialAnimationGroup, QPoint, QSize)

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEndValue(QPoint(200, 200))
        self.anim.setDuration(1500)
        self.anim_2 = QPropertyAnimation(self.child, b"size")
        self.anim_2.setEndValue(QSize(250, 150))
        self.anim_2.setDuration(2000)
        self.anim_group = QSequentialAnimationGroup()
        self.anim_group.addAnimation(self.anim)
        self.anim_group.addAnimation(self.anim_2)
        self.anim_group.start()

python
from PySide6.QtWidgets import QWidget
from PySide6.QtCore import (
    QPropertyAnimation, QSequentialAnimationGroup, QPoint, QSize)


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEndValue(QPoint(200, 200))
        self.anim.setDuration(1500)
        self.anim_2 = QPropertyAnimation(self.child, b"size")
        self.anim_2.setEndValue(QSize(250, 150))
        self.anim_2.setDuration(2000)
        self.anim_group = QSequentialAnimationGroup()
        self.anim_group.addAnimation(self.anim)
        self.anim_group.addAnimation(self.anim_2)
        self.anim_group.start()

Two sequential animations Chaining two sequential animations one after another.

Alternatively, you can run multiple animations concurrently. The following example applies two animations that run in parallel. The first moves the block as before, the second fades the block in.

python
from PySide2.QtWidgets import QWidget, QGraphicsOpacityEffect
from PySide2.QtCore import QPropertyAnimation, QParallelAnimationGroup, QPoint

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        effect = QGraphicsOpacityEffect(self.child)
        self.child.setGraphicsEffect(effect)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEndValue(QPoint(200, 200))
        self.anim.setDuration(1500)
        self.anim_2 = QPropertyAnimation(effect, b"opacity")
        self.anim_2.setStartValue(0)
        self.anim_2.setEndValue(1)
        self.anim_2.setDuration(2500)
        self.anim_group = QParallelAnimationGroup()
        self.anim_group.addAnimation(self.anim)
        self.anim_group.addAnimation(self.anim_2)
        self.anim_group.start()

python
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
from PySide6.QtCore import QPropertyAnimation, QParallelAnimationGroup, QPoint


class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(600, 600)
        self.child = QWidget(self)
        effect = QGraphicsOpacityEffect(self.child)
        self.child.setGraphicsEffect(effect)
        self.child.setStyleSheet("background-color:red;border-radius:15px;")
        self.child.resize(100, 100)
        self.anim = QPropertyAnimation(self.child, b"pos")
        self.anim.setEndValue(QPoint(200, 200))
        self.anim.setDuration(1500)
        self.anim_2 = QPropertyAnimation(effect, b"opacity")
        self.anim_2.setStartValue(0)
        self.anim_2.setEndValue(1)
        self.anim_2.setDuration(2500)
        self.anim_group = QParallelAnimationGroup()
        self.anim_group.addAnimation(self.anim)
        self.anim_group.addAnimation(self.anim_2)
        self.anim_group.start()

Two parallel animations Running two concurrent animations on a single widget.

Animated Toggle "Replacement for QCheckBox"

With these simple building blocks we have everything we need to build complex UI behaviors into our custom widgets. In this next part we'll take what we've learnt and use it to construct a fully-functional custom "Toggle" widget with animated behaviors.

The widget we're building inherits from QCheckBox and provides a drop-in replacement for it, adding an animated toggle switch with animated slider and a little bit of eye candy to highlight state changes. By inheriting from QCheckBox we get all the built-in checkbox behaviors for free, so we just need to deal with the visual parts.

To implement our design, we --

  • Define our colors (QPen and QBrush) using arguments and store them as object attributes. This is not required, but saves us constructing them on every frame.
  • Override the paintEvent(self, e) which receives a QPaintEvent.
  • Define QPropertyAnimation and QAnimationGroup objects, to control the properties we want to animate.
  • Select the correct signals on which to trigger the animation.

Below is the complete code for our custom animated toggle checkbox.

python
from PySide2.QtCore import (
    Qt, QSize, QPoint, QPointF, QRectF,
    QEasingCurve, QPropertyAnimation, QSequentialAnimationGroup,
    Slot, Property)

from PySide2.QtWidgets import QCheckBox
from PySide2.QtGui import QColor, QBrush, QPaintEvent, QPen, QPainter


class AnimatedToggle(QCheckBox):

    _transparent_pen = QPen(Qt.transparent)
    _light_grey_pen = QPen(Qt.lightGray)

    def __init__(self,
        parent=None,
        bar_color=Qt.gray,
        checked_color="#00B0FF",
        handle_color=Qt.white,
        pulse_unchecked_color="#44999999",
        pulse_checked_color="#4400B0EE"
        ):
        super().__init__(parent)

        # Save our properties on the object via self, so we can access them later
        # in the paintEvent.
        self._bar_brush = QBrush(bar_color)
        self._bar_checked_brush = QBrush(QColor(checked_color).lighter())

        self._handle_brush = QBrush(handle_color)
        self._handle_checked_brush = QBrush(QColor(checked_color))

        self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color))
        self._pulse_checked_animation = QBrush(QColor(pulse_checked_color))

        # Setup the rest of the widget.

        self.setContentsMargins(8, 0, 8, 0)
        self._handle_position = 0

        self._pulse_radius = 0

        self.animation = QPropertyAnimation(self, b"handle_position", self)
        self.animation.setEasingCurve(QEasingCurve.InOutCubic)
        self.animation.setDuration(200)  # time in ms

        self.pulse_anim = QPropertyAnimation(self, b"pulse_radius", self)
        self.pulse_anim.setDuration(350)  # time in ms
        self.pulse_anim.setStartValue(10)
        self.pulse_anim.setEndValue(20)

        self.animations_group = QSequentialAnimationGroup()
        self.animations_group.addAnimation(self.animation)
        self.animations_group.addAnimation(self.pulse_anim)

        self.stateChanged.connect(self.setup_animation)

    def sizeHint(self):
        return QSize(58, 45)

    def hitButton(self, pos: QPoint):
        return self.contentsRect().contains(pos)

    @Slot(int)
    def setup_animation(self, value):
        self.animations_group.stop()
        if value:
            self.animation.setEndValue(1)
        else:
            self.animation.setEndValue(0)
        self.animations_group.start()

    def paintEvent(self, e: QPaintEvent):

        contRect = self.contentsRect()
        handleRadius = round(0.24 * contRect.height())

        p = QPainter(self)
        p.setRenderHint(QPainter.Antialiasing)

        p.setPen(self._transparent_pen)
        barRect = QRectF(
            0, 0,
            contRect.width() - handleRadius, 0.40 * contRect.height()
        )
        barRect.moveCenter(contRect.center())
        rounding = barRect.height() / 2

        # the handle will move along this line
        trailLength = contRect.width() - 2 * handleRadius

        xPos = contRect.x() + handleRadius + trailLength * self._handle_position

        if self.pulse_anim.state() == QPropertyAnimation.Running:
            p.setBrush(
                self._pulse_checked_animation if
                self.isChecked() else self._pulse_unchecked_animation)
            p.drawEllipse(QPointF(xPos, barRect.center().y()),
                          self._pulse_radius, self._pulse_radius)

        if self.isChecked():
            p.setBrush(self._bar_checked_brush)
            p.drawRoundedRect(barRect, rounding, rounding)
            p.setBrush(self._handle_checked_brush)

        else:
            p.setBrush(self._bar_brush)
            p.drawRoundedRect(barRect, rounding, rounding)
            p.setPen(self._light_grey_pen)
            p.setBrush(self._handle_brush)

        p.drawEllipse(
            QPointF(xPos, barRect.center().y()),
            handleRadius, handleRadius)

        p.end()

    @Property(float)
    def handle_position(self):
        return self._handle_position

    @handle_position.setter
    def handle_position(self, pos):
        """change the property
        we need to trigger QWidget.update() method, either by:
            1- calling it here [ what we're doing ].
            2- connecting the QPropertyAnimation.valueChanged() signal to it.
        """
        self._handle_position = pos
        self.update()

    @Property(float)
    def pulse_radius(self):
        return self._pulse_radius

    @pulse_radius.setter
    def pulse_radius(self, pos):
        self._pulse_radius = pos
        self.update()

python
from PySide6.QtCore import (
    Qt, QSize, QPoint, QPointF, QRectF,
    QEasingCurve, QPropertyAnimation, QSequentialAnimationGroup,
    Slot, Property)

from PySide6.QtWidgets import QCheckBox
from PySide6.QtGui import QColor, QBrush, QPaintEvent, QPen, QPainter


class AnimatedToggle(QCheckBox):

    _transparent_pen = QPen(Qt.transparent)
    _light_grey_pen = QPen(Qt.lightGray)

    def __init__(self,
        parent=None,
        bar_color=Qt.gray,
        checked_color="#00B0FF",
        handle_color=Qt.white,
        pulse_unchecked_color="#44999999",
        pulse_checked_color="#4400B0EE"
        ):
        super().__init__(parent)

        # Save our properties on the object via self, so we can access them later
        # in the paintEvent.
        self._bar_brush = QBrush(bar_color)
        self._bar_checked_brush = QBrush(QColor(checked_color).lighter())

        self._handle_brush = QBrush(handle_color)
        self._handle_checked_brush = QBrush(QColor(checked_color))

        self._pulse_unchecked_animation = QBrush(QColor(pulse_unchecked_color))
        self._pulse_checked_animation = QBrush(QColor(pulse_checked_color))

        # Setup the rest of the widget.

        self.setContentsMargins(8, 0, 8, 0)
        self._handle_position = 0

        self._pulse_radius = 0

        self.animation = QPropertyAnimation(self, b"handle_position", self)
        self.animation.setEasingCurve(QEasingCurve.InOutCubic)
        self.animation.setDuration(200)  # time in ms

        self.pulse_anim = QPropertyAnimation(self, b"pulse_radius", self)
        self.pulse_anim.setDuration(350)  # time in ms
        self.pulse_anim.setStartValue(10)
        self.pulse_anim.setEndValue(20)

        self.animations_group = QSequentialAnimationGroup()
        self.animations_group.addAnimation(self.animation)
        self.animations_group.addAnimation(self.pulse_anim)

        self.stateChanged.connect(self.setup_animation)

    def sizeHint(self):
        return QSize(58, 45)

    def hitButton(self, pos: QPoint):
        return self.contentsRect().contains(pos)

    @Slot(int)
    def setup_animation(self, value):
        self.animations_group.stop()
        if value:
            self.animation.setEndValue(1)
        else:
            self.animation.setEndValue(0)
        self.animations_group.start()

    def paintEvent(self, e: QPaintEvent):

        contRect = self.contentsRect()
        handleRadius = round(0.24 * contRect.height())

        p = QPainter(self)
        p.setRenderHint(QPainter.Antialiasing)

        p.setPen(self._transparent_pen)
        barRect = QRectF(
            0, 0,
            contRect.width() - handleRadius, 0.40 * contRect.height()
        )
        barRect.moveCenter(contRect.center())
        rounding = barRect.height() / 2

        # the handle will move along this line
        trailLength = contRect.width() - 2 * handleRadius

        xPos = contRect.x() + handleRadius + trailLength * self._handle_position

        if self.pulse_anim.state() == QPropertyAnimation.Running:
            p.setBrush(
                self._pulse_checked_animation if
                self.isChecked() else self._pulse_unchecked_animation)
            p.drawEllipse(QPointF(xPos, barRect.center().y()),
                          self._pulse_radius, self._pulse_radius)

        if self.isChecked():
            p.setBrush(self._bar_checked_brush)
            p.drawRoundedRect(barRect, rounding, rounding)
            p.setBrush(self._handle_checked_brush)

        else:
            p.setBrush(self._bar_brush)
            p.drawRoundedRect(barRect, rounding, rounding)
            p.setPen(self._light_grey_pen)
            p.setBrush(self._handle_brush)

        p.drawEllipse(
            QPointF(xPos, barRect.center().y()),
            handleRadius, handleRadius)

        p.end()

    @Property(float)
    def handle_position(self):
        return self._handle_position

    @handle_position.setter
    def handle_position(self, pos):
        """change the property
        we need to trigger QWidget.update() method, either by:
            1- calling it here [ what we're doing ].
            2- connecting the QPropertyAnimation.valueChanged() signal to it.
        """
        self._handle_position = pos
        self.update()

    @Property(float)
    def pulse_radius(self):
        return self._pulse_radius

    @pulse_radius.setter
    def pulse_radius(self, pos):
        self._pulse_radius = pos
        self.update()

The AnimatedToggle class is quite complex. There are a few key points to notice.

  1. Because we're inheriting from QCheckBox it is essential that we override hitButton(). This defines the clickable area of our widget, and by a QCheckBox is only clickable in the area of the checkable box. Here we expand the clickable region to the entire widget, using self.contentsRect() so a click anywhere on the widget will toggle the state.
  2. Similarly it's essential we override sizeHint() so when we add our widget to layouts, they know an acceptable default size to use.
  3. You must set p.setRenderHint(QPainter.Antialiasing) to smooth the edges of things you draw, otherwise the outline will be jagged.
  4. In this example we trigger the animation using the self.stateChanged signal, which is provided by QCheckBox. This fires whenever the state (checked or unchecked) of the widget changes. It is important to choose the right trigger to start the animation in order for the widget to feel intuitive.
  5. Since we're using stateChanged to start the animation, if you check the state of the toggle as soon as it's been clicked it will give the correct value -- even if the animation is not yet complete.

Don't try to change the logical state inside paintEvent or from a QPropertyAnimation.

Save the above code in a file named animated_toggle.py and in the same folder save the following simple skeleton application (e.g. as app.py) which imports the AnimatedToggle class and creates a little demo.

python
from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel
from animated_toggle import AnimatedToggle

app = QApplication([])

window = QWidget()

mainToggle = AnimatedToggle()
secondaryToggle = AnimatedToggle(
        checked_color="#FFB000",
        pulse_checked_color="#44FFB000"
)
mainToggle.setFixedSize(mainToggle.sizeHint())
secondaryToggle.setFixedSize(mainToggle.sizeHint())

window.setLayout(QVBoxLayout())
window.layout().addWidget(QLabel("Main Toggle"))
window.layout().addWidget(mainToggle)

window.layout().addWidget(QLabel("Secondary Toggle"))
window.layout().addWidget(secondaryToggle)

mainToggle.stateChanged.connect(secondaryToggle.setChecked)

window.show()
app.exec_()

python
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel
from animated_toggle import AnimatedToggle

app = QApplication([])

window = QWidget()

mainToggle = AnimatedToggle()
secondaryToggle = AnimatedToggle(
        checked_color="#FFB000",
        pulse_checked_color="#44FFB000"
)
mainToggle.setFixedSize(mainToggle.sizeHint())
secondaryToggle.setFixedSize(mainToggle.sizeHint())

window.setLayout(QVBoxLayout())
window.layout().addWidget(QLabel("Main Toggle"))
window.layout().addWidget(mainToggle)

window.layout().addWidget(QLabel("Secondary Toggle"))
window.layout().addWidget(secondaryToggle)

mainToggle.stateChanged.connect(secondaryToggle.setChecked)

window.show()
app.exec_()

Running the code you should see the following demo appear in a window.

Window with two animated checkbox toggles The custom animated toggle checkbox in action.

Try experimenting with the AnimatedToggle class, using alternative easing curves and building different animation sequences. See what you can come up with!


  1. well, decorators are functions too. 

For an in-depth guide to building Python GUIs with PySide2 see my book, Create GUI Applications with Python & Qt5.