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 tomain.py
,dialog.cpp/h
, which was translated todialog.py
,wigglywidget.cpp/h
, which was translated towigglywidget.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 exposeCMakeList.txt
, with all the instructions to build the shared libraries (DLL, or dylib)
Now create a build/
directory, and from inside run cmake
to use
the provided CMakeLists.txt
:
Run CMake on macOS/Linux:
cd ~/pyside-setup/examples/widgetbinding
cd build
cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release
Run CMake on Windows:
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:
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.
// 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
<?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>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
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
from __future__ import annotations
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 __future__ import annotations
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 __future__ import annotations
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)
# 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)
# ================================== Dependency detection ======================================
# Find required packages
find_package(Python COMPONENTS Interpreter Development REQUIRED)
# On RHEL and some other distros, Python wheels and site-packages may be installed under 'lib64'
# instead of 'lib'. The FindPython CMake module may set Python_SITELIB to 'lib', which is incorrect
# for these cases. To ensure compatibility, we override Python_SITELIB by querying Python directly.
# This guarantees the correct site-packages path is used regardless of platform or Python build.
execute_process(
COMMAND ${Python_EXECUTABLE} -c
"import site; print(next(p for p in site.getsitepackages() if 'site-packages' in p))"
OUTPUT_VARIABLE Python_SITELIB
OUTPUT_STRIP_TRAILING_WHITESPACE
)
list(APPEND CMAKE_PREFIX_PATH
"${Python_SITELIB}/shiboken6_generator/lib/cmake"
)
find_package(Shiboken6Tools REQUIRED)
# ==================================== 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 ${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)
target_link_libraries(${wiggly_library} PRIVATE Qt6::Widgets)
# ====================== Shiboken target for generating binding C++ files ====================
# Define Qt modules needed
set(qt_modules Core Gui Widgets)
# Create Python bindings using Shiboken6Tools function
shiboken_generator_create_binding(
EXTENSION_TARGET ${bindings_library}
GENERATED_SOURCES ${generated_sources}
HEADERS ${wrapped_header}
TYPESYSTEM_FILE ${typesystem_file}
LIBRARY_TARGET ${wiggly_library}
QT_MODULES Core Gui Widgets
)
# ================================= 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}")
# Get the correct DLL path for the current build type
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
get_target_property(dll_path Shiboken6::libshiboken IMPORTED_LOCATION_DEBUG)
else()
get_target_property(dll_path Shiboken6::libshiboken IMPORTED_LOCATION_RELEASE)
endif()
file(TO_CMAKE_PATH "${dll_path}" dll_path)
set(windows_shiboken_shared_libraries "${dll_path}")
# =========================================================================================
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.
# =============================================================================================