WigglyWidget#

The original Qt/C++ example can be found here: https://doc.qt.io/qt-6/qtwidgets-widgets-wiggly-example.html

This example shows how to interact with a custom widget from two different ways:

  • A full Python translation from a C++ example,

  • A Python binding generated from the C++ file.

The original example contained three different files:

  • main.cpp/h, which was translated to main.py,

  • dialog.cpp/h, which was translated to dialog.py,

  • wigglywidget.cpp/h, which was translated to wigglywidget.py, but also remains as is, to enable the binding generation through Shiboken.

In the dialog.py file you will find two imports that will be related to each of the two approaches described before::

# Python translated file
from wigglywidget import WigglyWidget

# Binding module create with Shiboken
from wiggly import WigglyWidget

Steps to build the bindings#

The most important files are:

  • bindings.xml, to specify the class that we want to expose from C++ to Python,

  • bindings.h to include the header of the classes we want to expose

  • CMakeList.txt, with all the instructions to build the shared libraries (DLL, or dylib)

  • pyside_config.py which is located in the utils directory, one level up, to get the path for Shiboken and PySide.

Now create a build/ directory, and from inside run cmake to use the provided CMakeLists.txt:

macOS/Linux:

cd ~/pyside-setup/examples/widgetbinding

On Windows:

cd C:\pyside-setup\examples\widgetbinding
mkdir build
cd build
cmake -H.. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja
ninja install
cd ..

The final example can then be run by:

python main.py

You should see two identical custom widgets, one being the Python translation, and the other one being the C++ one.

Final words#

Since this example originated by mixing the concepts of the scriptableapplication and samplebinding examples, you can complement this README with the ones in those directories.

Download this example

// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef BINDINGS_H
#define BINDINGS_H
#include "wigglywidget.h"
#endif // BINDINGS_H
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtWidgets import QDialog, QLineEdit, QVBoxLayout

# Python binding from the C++ widget
from wiggly import WigglyWidget as WigglyWidgetCPP

# Python-only widget
from wigglywidget import WigglyWidget as WigglyWidgetPY


class Dialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        wiggly_widget_py = WigglyWidgetPY(self)
        wiggly_widget_cpp = WigglyWidgetCPP(self)
        lineEdit = QLineEdit(self)

        layout = QVBoxLayout(self)
        layout.addWidget(wiggly_widget_py)
        layout.addWidget(wiggly_widget_cpp)
        layout.addWidget(lineEdit)
        lineEdit.setClearButtonEnabled(True)
        wiggly_widget_py.running = True
        wiggly_widget_cpp.setRunning(True)

        lineEdit.textChanged.connect(wiggly_widget_py.setText)
        lineEdit.textChanged.connect(wiggly_widget_cpp.setText)
        lineEdit.setText("🖖 Hello world!")

        self.setWindowTitle("Wiggly")
        self.resize(360, 145)
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef MACROS_H
#define MACROS_H

#include <QtCore/qglobal.h>

// Export symbols when creating .dll and .lib, and import them when using .lib.
#if BINDINGS_BUILD
#    define BINDINGS_API Q_DECL_EXPORT
#else
#    define BINDINGS_API Q_DECL_IMPORT
#endif

#endif // MACROS_H
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

import sys

from PySide6.QtWidgets import QApplication

from dialog import Dialog

if __name__ == "__main__":
    app = QApplication()
    w = Dialog()
    w.show()
    sys.exit(app.exec())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from wigglywidget import WigglyWidget

# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin


TOOLTIP = "A cool wiggly widget (Python)"
DOM_XML = """
<ui language='c++'>
    <widget class='WigglyWidget' name='wigglyWidget'>
        <property name='geometry'>
            <rect>
                <x>0</x>
                <y>0</y>
                <width>400</width>
                <height>200</height>
            </rect>
        </property>
        <property name='text'>
            <string>Hello, world</string>
        </property>
    </widget>
</ui>
"""

if __name__ == '__main__':
    QPyDesignerCustomWidgetCollection.registerCustomWidget(WigglyWidget, module="wigglywidget",
                                                           tool_tip=TOOLTIP, xml=DOM_XML)
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include "wigglywidget.h"

#include <QtGui/QFontMetrics>
#include <QtGui/QPainter>
#include <QtCore/QTimerEvent>

//! [0]
WigglyWidget::WigglyWidget(QWidget *parent)
    : QWidget(parent)
{
    setBackgroundRole(QPalette::Midlight);
    setAutoFillBackground(true);

    QFont newFont = font();
    newFont.setPointSize(newFont.pointSize() + 20);
    setFont(newFont);
}
//! [0]

//! [1]
void WigglyWidget::paintEvent(QPaintEvent * /* event */)
//! [1] //! [2]
{
    if (m_text.isEmpty())
        return;
    static constexpr int sineTable[16] = {
        0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100, -92, -71, -38
    };

    QFontMetrics metrics(font());
    int x = (width() - metrics.horizontalAdvance(m_text)) / 2;
    int y = (height() + metrics.ascent() - metrics.descent()) / 2;
    QColor color;
//! [2]

//! [3]
    QPainter painter(this);
//! [3] //! [4]
    int offset = 0;
    const auto codePoints = m_text.toUcs4();
    for (char32_t codePoint : codePoints) {
        const int index = (m_step + offset++) % 16;
        color.setHsv((15 - index) * 16, 255, 191);
        painter.setPen(color);
        QString symbol = QString::fromUcs4(&codePoint, 1);
        const int dy = (sineTable[index] * metrics.height()) / 400;
        painter.drawText(x, y - dy, symbol);
        x += metrics.horizontalAdvance(symbol);
    }
}
//! [4]

//! [5]
void WigglyWidget::timerEvent(QTimerEvent *event)
//! [5] //! [6]
{
    if (event->timerId() == m_timer.timerId()) {
        ++m_step;
        update();
    } else {
        QWidget::timerEvent(event);
    }
//! [6]
}

QString WigglyWidget::text() const
{
    return m_text;
}

void WigglyWidget::setText(const QString &newText)
{
    m_text = newText;
}

bool WigglyWidget::isRunning() const
{
    return m_timer.isActive();
}

void WigglyWidget::setRunning(bool r)
{
    if (r == isRunning())
        return;
    if (r)
        m_timer.start(60, this);
    else
        m_timer.stop();
}
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef WIGGLYWIDGET_H
#define WIGGLYWIDGET_H

#include "macros.h"

#include <QtWidgets/QWidget>
#include <QtCore/QBasicTimer>

//! [0]
class BINDINGS_API WigglyWidget : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(bool running READ isRunning WRITE setRunning)
    Q_PROPERTY(QString text READ text WRITE setText)

public:
    WigglyWidget(QWidget *parent = nullptr);

    QString text() const;
    bool isRunning() const;

public slots:
    void setText(const QString &newText);
    void setRunning(bool r);

protected:
    void paintEvent(QPaintEvent *event) override;
    void timerEvent(QTimerEvent *event) override;

private:
    QBasicTimer m_timer;
    QString m_text;
    int m_step = 0;
};
//! [0]

#endif
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

from PySide6.QtCore import QBasicTimer, Property
from PySide6.QtGui import QColor, QFontMetrics, QPainter, QPalette
from PySide6.QtWidgets import QWidget


class WigglyWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._step = 0
        self._text = ""
        self.setBackgroundRole(QPalette.Midlight)
        self.setAutoFillBackground(True)

        new_font = self.font()
        new_font.setPointSize(new_font.pointSize() + 20)
        self.setFont(new_font)
        self._timer = QBasicTimer()

    def isRunning(self):
        return self._timer.isActive()

    def setRunning(self, r):
        if r == self.isRunning():
            return
        if r:
            self._timer.start(60, self)
        else:
            self._timer.stop()

    def paintEvent(self, event):
        if not self._text:
            return

        sineTable = [0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100,
                     -92, -71, -38]

        metrics = QFontMetrics(self.font())
        x = (self.width() - metrics.horizontalAdvance(self.text)) / 2
        y = (self.height() + metrics.ascent() - metrics.descent()) / 2
        color = QColor()

        with QPainter(self) as painter:
            for i in range(len(self.text)):
                index = (self._step + i) % 16
                color.setHsv((15 - index) * 16, 255, 191)
                painter.setPen(color)
                dy = (sineTable[index] * metrics.height()) / 400
                c = self._text[i]
                painter.drawText(x, y - dy, str(c))
                x += metrics.horizontalAdvance(c)

    def timerEvent(self, event):
        if event.timerId() == self._timer.timerId():
            self._step += 1
            self.update()
        else:
            QWidget.timerEvent(event)

    def text(self):
        return self._text

    def setText(self, text):
        self._text = text

    running = Property(bool, isRunning, setRunning)
    text = Property(str, text, setText)