aboutsummaryrefslogtreecommitdiffstats
path: root/examples/widgetbinding
diff options
context:
space:
mode:
Diffstat (limited to 'examples/widgetbinding')
-rw-r--r--examples/widgetbinding/CMakeLists.txt294
-rw-r--r--examples/widgetbinding/bindings.h7
-rw-r--r--examples/widgetbinding/bindings.xml9
-rw-r--r--examples/widgetbinding/dialog.py33
-rw-r--r--examples/widgetbinding/doc/widgetbinding.md78
-rw-r--r--examples/widgetbinding/doc/widgetbinding.pyproject10
-rw-r--r--examples/widgetbinding/macros.h16
-rw-r--r--examples/widgetbinding/main.py14
-rw-r--r--examples/widgetbinding/registerwigglywidget.py31
-rw-r--r--examples/widgetbinding/wigglywidget.cpp92
-rw-r--r--examples/widgetbinding/wigglywidget.h40
-rw-r--r--examples/widgetbinding/wigglywidget.py69
12 files changed, 693 insertions, 0 deletions
diff --git a/examples/widgetbinding/CMakeLists.txt b/examples/widgetbinding/CMakeLists.txt
new file mode 100644
index 000000000..1c5eefa50
--- /dev/null
+++ b/examples/widgetbinding/CMakeLists.txt
@@ -0,0 +1,294 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.18)
+cmake_policy(VERSION 3.18)
+
+# Enable policy to not use RPATH settings for install_name on macOS.
+if(POLICY CMP0068)
+ cmake_policy(SET CMP0068 NEW)
+endif()
+
+# Enable policy to run automoc on generated files.
+if(POLICY CMP0071)
+ cmake_policy(SET CMP0071 NEW)
+endif()
+
+# Consider changing the project name to something relevant for you.
+project(wiggly LANGUAGES CXX)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_AUTOMOC ON)
+
+find_package(Qt6 COMPONENTS Core Gui Widgets)
+
+# ================================ General configuration ======================================
+
+# Set CPP standard to C++17 minimum.
+set(CMAKE_CXX_STANDARD 17)
+
+# The wiggly library for which we will create bindings. You can change the name to something
+# relevant for your project.
+set(wiggly_library "libwiggly")
+
+# The name of the generated bindings module (as imported in Python). You can change the name
+# to something relevant for your project.
+set(bindings_library "wiggly")
+
+# The header file with all the types and functions for which bindings will be generated.
+# Usually it simply includes other headers of the library you are creating bindings for.
+set(wrapped_header ${CMAKE_SOURCE_DIR}/bindings.h)
+
+# The typesystem xml file which defines the relationships between the C++ types / functions
+# and the corresponding Python equivalents.
+set(typesystem_file ${CMAKE_SOURCE_DIR}/bindings.xml)
+
+# Specify which C++ files will be generated by shiboken. This includes the module wrapper
+# and a '.cpp' file per C++ type. These are needed for generating the module shared
+# library.
+set(generated_sources
+ ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/wiggly_module_wrapper.cpp
+ ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/wigglywidget_wrapper.cpp)
+
+
+# ================================== Shiboken detection ======================================
+# Use provided python interpreter if given.
+if(NOT python_interpreter)
+ if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
+ find_program(python_interpreter "python_d")
+ if(NOT python_interpreter)
+ message(FATAL_ERROR
+ "A debug Python interpreter could not be found, which is a requirement when "
+ "building this example in a debug configuration. Make sure python_d.exe is in "
+ "PATH.")
+ endif()
+ else()
+ find_program(python_interpreter "python")
+ if(NOT python_interpreter)
+ message(FATAL_ERROR
+ "No Python interpreter could be found. Make sure python is in PATH.")
+ endif()
+ endif()
+endif()
+message(STATUS "Using python interpreter: ${python_interpreter}")
+
+# Macro to get various pyside / python include / link flags and paths.
+# Uses the not entirely supported utils/pyside_config.py file.
+macro(pyside_config option output_var)
+ if(${ARGC} GREATER 2)
+ set(is_list ${ARGV2})
+ else()
+ set(is_list "")
+ endif()
+
+ execute_process(
+ COMMAND ${python_interpreter} "${CMAKE_SOURCE_DIR}/../utils/pyside_config.py"
+ ${option}
+ OUTPUT_VARIABLE ${output_var}
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+
+ if ("${${output_var}}" STREQUAL "")
+ message(FATAL_ERROR "Error: Calling pyside_config.py ${option} returned no output.")
+ endif()
+ if(is_list)
+ string (REPLACE " " ";" ${output_var} "${${output_var}}")
+ endif()
+endmacro()
+
+# Query for the shiboken generator path, Python path, include paths and linker flags.
+pyside_config(--shiboken-module-path shiboken_module_path)
+pyside_config(--shiboken-generator-path shiboken_generator_path)
+pyside_config(--pyside-path pyside_path)
+pyside_config(--pyside-include-path pyside_include_dir 1)
+pyside_config(--python-include-path python_include_dir)
+pyside_config(--shiboken-generator-include-path shiboken_include_dir 1)
+pyside_config(--shiboken-module-shared-libraries-cmake shiboken_shared_libraries 0)
+pyside_config(--python-link-flags-cmake python_linking_data 0)
+pyside_config(--pyside-shared-libraries-cmake pyside_shared_libraries 0)
+
+set(shiboken_path "${shiboken_generator_path}/shiboken6${CMAKE_EXECUTABLE_SUFFIX}")
+if(NOT EXISTS ${shiboken_path})
+ message(FATAL_ERROR "Shiboken executable not found at path: ${shiboken_path}")
+endif()
+
+
+# ==================================== RPATH configuration ====================================
+
+
+# =============================================================================================
+# !!! (The section below is deployment related, so in a real world application you will want to
+# take care of this properly with some custom script or tool).
+# =============================================================================================
+# Enable rpaths so that the built shared libraries find their dependencies.
+set(CMAKE_SKIP_BUILD_RPATH FALSE)
+set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
+set(CMAKE_INSTALL_RPATH ${shiboken_module_path} ${CMAKE_CURRENT_SOURCE_DIR})
+set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
+# =============================================================================================
+# !!! End of dubious section.
+# =============================================================================================
+
+
+# =============================== CMake target - wiggly_library ===============================
+
+
+# Get the relevant Qt include dirs, to pass them on to shiboken.
+get_property(QT_WIDGETS_INCLUDE_DIRS TARGET Qt6::Widgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
+set(INCLUDES "")
+foreach(INCLUDE_DIR ${QT_WIDGETS_INCLUDE_DIRS})
+ list(APPEND INCLUDES "-I${INCLUDE_DIR}")
+endforeach()
+
+# On macOS, check if Qt is a framework build. This affects how include paths should be handled.
+get_target_property(QtCore_is_framework Qt6::Core FRAMEWORK)
+if (QtCore_is_framework)
+ get_target_property(qt_core_library_location Qt6::Core LOCATION)
+ get_filename_component(qt_core_library_location_dir "${qt_core_library_location}" DIRECTORY)
+ get_filename_component(lib_dir "${qt_core_library_location_dir}/../" ABSOLUTE)
+ list(APPEND INCLUDES "--framework-include-paths=${lib_dir}")
+endif()
+
+# We need to include the headers for the module bindings that we use.
+set(pyside_additional_includes "")
+foreach(INCLUDE_DIR ${pyside_include_dir})
+ list(APPEND pyside_additional_includes "${INCLUDE_DIR}/QtCore")
+ list(APPEND pyside_additional_includes "${INCLUDE_DIR}/QtGui")
+ list(APPEND pyside_additional_includes "${INCLUDE_DIR}/QtWidgets")
+endforeach()
+
+
+# Define the wiggly shared library for which we will create bindings.
+set(${wiggly_library}_sources wigglywidget.cpp)
+add_library(${wiggly_library} SHARED ${${wiggly_library}_sources})
+set_property(TARGET ${wiggly_library} PROPERTY PREFIX "")
+
+# Needed mostly on Windows to export symbols, and create a .lib file, otherwise the binding
+# library can't link to the wiggly library.
+target_compile_definitions(${wiggly_library} PRIVATE BINDINGS_BUILD)
+
+
+# ====================== Shiboken target for generating binding C++ files ====================
+
+
+# Set up the options to pass to shiboken.
+set(shiboken_options --generator-set=shiboken --enable-parent-ctor-heuristic
+ --enable-pyside-extensions --enable-return-value-heuristic --use-isnull-as-nb_nonzero
+ --avoid-protected-hack
+ ${INCLUDES}
+ -I${CMAKE_SOURCE_DIR}
+ -T${CMAKE_SOURCE_DIR}
+ -T${pyside_path}/typesystems
+ --output-directory=${CMAKE_CURRENT_BINARY_DIR}
+ )
+
+set(generated_sources_dependencies ${wrapped_header} ${typesystem_file})
+
+# Add custom target to run shiboken to generate the binding cpp files.
+add_custom_command(OUTPUT ${generated_sources}
+ COMMAND ${shiboken_path}
+ ${shiboken_options} ${wrapped_header} ${typesystem_file}
+ DEPENDS ${generated_sources_dependencies}
+ #IMPLICIT_DEPENDS CXX ${wrapped_header}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ COMMENT "Running generator for ${typesystem_file}.")
+
+
+# =============================== CMake target - bindings_library =============================
+
+
+# Set the cpp files which will be used for the bindings library.
+set(${bindings_library}_sources ${generated_sources})
+
+# Define and build the bindings library.
+add_library(${bindings_library} SHARED ${${bindings_library}_sources})
+
+
+# Apply relevant include and link flags.
+target_include_directories(${bindings_library} PRIVATE ${pyside_additional_includes})
+target_include_directories(${bindings_library} PRIVATE ${pyside_include_dir})
+target_include_directories(${bindings_library} PRIVATE ${python_include_dir})
+target_include_directories(${bindings_library} PRIVATE ${shiboken_include_dir})
+
+target_link_libraries(${wiggly_library} PRIVATE Qt6::Widgets)
+target_link_libraries(${bindings_library} PRIVATE Qt6::Widgets)
+target_link_libraries(${bindings_library} PRIVATE ${wiggly_library})
+target_link_libraries(${bindings_library} PRIVATE ${pyside_shared_libraries})
+target_link_libraries(${bindings_library} PRIVATE ${shiboken_shared_libraries})
+
+# Adjust the name of generated module.
+set_property(TARGET ${bindings_library} PROPERTY PREFIX "")
+set_property(TARGET ${bindings_library} PROPERTY OUTPUT_NAME
+ "${bindings_library}${PYTHON_EXTENSION_SUFFIX}")
+if(WIN32)
+ if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
+ set_property(TARGET ${bindings_library} PROPERTY SUFFIX "_d.pyd")
+ else()
+ set_property(TARGET ${bindings_library} PROPERTY SUFFIX ".pyd")
+ endif()
+endif()
+
+# Make sure the linker doesn't complain about not finding Python symbols on macOS.
+if(APPLE)
+ set_target_properties(${bindings_library} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
+endif(APPLE)
+
+# Find and link to the python import library only on Windows.
+# On Linux and macOS, the undefined symbols will get resolved by the dynamic linker
+# (the symbols will be picked up in the Python executable).
+if (WIN32)
+ list(GET python_linking_data 0 python_libdir)
+ list(GET python_linking_data 1 python_lib)
+ find_library(python_link_flags ${python_lib} PATHS ${python_libdir} HINTS ${python_libdir})
+ target_link_libraries(${bindings_library} PRIVATE ${python_link_flags})
+endif()
+
+# ================================= Dubious deployment section ================================
+
+set(windows_shiboken_shared_libraries)
+
+if(WIN32)
+ # =========================================================================================
+ # !!! (The section below is deployment related, so in a real world application you will
+ # want to take care of this properly (this is simply to eliminate errors that users usually
+ # encounter.
+ # =========================================================================================
+ # Circumvent some "#pragma comment(lib)"s in "include/pyconfig.h" which might force to link
+ # against a wrong python shared library.
+
+ set(python_versions_list 3 36 37 38 39)
+ set(python_additional_link_flags "")
+ foreach(ver ${python_versions_list})
+ set(python_additional_link_flags
+ "${python_additional_link_flags} /NODEFAULTLIB:\"python${ver}_d.lib\"")
+ set(python_additional_link_flags
+ "${python_additional_link_flags} /NODEFAULTLIB:\"python${ver}.lib\"")
+ endforeach()
+
+ set_target_properties(${bindings_library}
+ PROPERTIES LINK_FLAGS "${python_additional_link_flags}")
+
+ # Compile a list of shiboken shared libraries to be installed, so that
+ # the user doesn't have to set the PATH manually to point to the PySide package.
+ foreach(library_path ${shiboken_shared_libraries})
+ string(REGEX REPLACE ".lib$" ".dll" library_path ${library_path})
+ file(TO_CMAKE_PATH ${library_path} library_path)
+ list(APPEND windows_shiboken_shared_libraries "${library_path}")
+ endforeach()
+ # =========================================================================================
+ # !!! End of dubious section.
+ # =========================================================================================
+endif()
+
+# =============================================================================================
+# !!! (The section below is deployment related, so in a real world application you will want to
+# take care of this properly with some custom script or tool).
+# =============================================================================================
+# Install the library and the bindings module into the source folder near the main.py file, so
+# that the Python interpeter successfully imports the used module.
+install(TARGETS ${bindings_library} ${wiggly_library}
+ LIBRARY DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}
+ RUNTIME DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}
+ )
+install(FILES ${windows_shiboken_shared_libraries} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR})
+# =============================================================================================
+# !!! End of dubious section.
+# =============================================================================================
diff --git a/examples/widgetbinding/bindings.h b/examples/widgetbinding/bindings.h
new file mode 100644
index 000000000..7b45ad1bf
--- /dev/null
+++ b/examples/widgetbinding/bindings.h
@@ -0,0 +1,7 @@
+// 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
diff --git a/examples/widgetbinding/bindings.xml b/examples/widgetbinding/bindings.xml
new file mode 100644
index 000000000..957684074
--- /dev/null
+++ b/examples/widgetbinding/bindings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+// Copyright (C) 2020 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+-->
+<typesystem package="wiggly">
+ <load-typesystem name="typesystem_widgets.xml" generate="no"/>
+ <object-type name="WigglyWidget"/>
+</typesystem>
diff --git a/examples/widgetbinding/dialog.py b/examples/widgetbinding/dialog.py
new file mode 100644
index 000000000..1bd127804
--- /dev/null
+++ b/examples/widgetbinding/dialog.py
@@ -0,0 +1,33 @@
+# 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)
diff --git a/examples/widgetbinding/doc/widgetbinding.md b/examples/widgetbinding/doc/widgetbinding.md
new file mode 100644
index 000000000..910961b1e
--- /dev/null
+++ b/examples/widgetbinding/doc/widgetbinding.md
@@ -0,0 +1,78 @@
+(widgetbinding-example)=
+# WigglyWidget Example
+
+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`:
+
+Run CMake on macOS/Linux:
+```bash
+cd ~/pyside-setup/examples/widgetbinding
+cd build
+cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release
+```
+
+Run CMake on Windows:
+```bash
+cd C:\pyside-setup\examples\widgetbinding
+mkdir build
+cd build
+cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=cl.exe
+```
+
+To build:
+```bash
+
+ninja
+ninja install
+cd ..
+```
+
+The final example can then be run by:
+```bash
+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.
diff --git a/examples/widgetbinding/doc/widgetbinding.pyproject b/examples/widgetbinding/doc/widgetbinding.pyproject
new file mode 100644
index 000000000..e423ea347
--- /dev/null
+++ b/examples/widgetbinding/doc/widgetbinding.pyproject
@@ -0,0 +1,10 @@
+{
+ "files": ["../bindings.h",
+ "../dialog.py",
+ "../macros.h",
+ "../main.py",
+ "../registerwigglywidget.py",
+ "../wigglywidget.cpp",
+ "../wigglywidget.h",
+ "../wigglywidget.py"]
+}
diff --git a/examples/widgetbinding/macros.h b/examples/widgetbinding/macros.h
new file mode 100644
index 000000000..001647966
--- /dev/null
+++ b/examples/widgetbinding/macros.h
@@ -0,0 +1,16 @@
+// 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
diff --git a/examples/widgetbinding/main.py b/examples/widgetbinding/main.py
new file mode 100644
index 000000000..2b24018b1
--- /dev/null
+++ b/examples/widgetbinding/main.py
@@ -0,0 +1,14 @@
+# 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())
diff --git a/examples/widgetbinding/registerwigglywidget.py b/examples/widgetbinding/registerwigglywidget.py
new file mode 100644
index 000000000..c5560ba6b
--- /dev/null
+++ b/examples/widgetbinding/registerwigglywidget.py
@@ -0,0 +1,31 @@
+# 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)
diff --git a/examples/widgetbinding/wigglywidget.cpp b/examples/widgetbinding/wigglywidget.cpp
new file mode 100644
index 000000000..6fdc65f39
--- /dev/null
+++ b/examples/widgetbinding/wigglywidget.cpp
@@ -0,0 +1,92 @@
+// 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();
+}
diff --git a/examples/widgetbinding/wigglywidget.h b/examples/widgetbinding/wigglywidget.h
new file mode 100644
index 000000000..e527a8f49
--- /dev/null
+++ b/examples/widgetbinding/wigglywidget.h
@@ -0,0 +1,40 @@
+// 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
diff --git a/examples/widgetbinding/wigglywidget.py b/examples/widgetbinding/wigglywidget.py
new file mode 100644
index 000000000..3362eab6b
--- /dev/null
+++ b/examples/widgetbinding/wigglywidget.py
@@ -0,0 +1,69 @@
+# 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)