Skip to content

Qt Hosting

This example hosts a Datoviz Vulkan scene in live Qt Widgets.

Preview

Qt Hosting

Run And Adapt

Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.

Route Availability Command or action
C++ Canonical native source DVZ_CMAKE_ARGS="-DDVZ_ENABLE_QT_BRIDGE=ON" just build, then ./build/examples/qt/qt_hosting
PyQt Additional integration source; check optional dependencies python3 -m examples.python.qt.hosted_pyqt
Browser Native only Qt and PyQt hosting use native toolkit windows and an optional bridge provider

Use this example as capability or integration evidence, not as a minimal copy-paste template. Start from the nearest supported, copy-safe example and add this feature after verifying the linked API reference.

Source

/*
 * Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for details.
 * SPDX-License-Identifier: MIT
 */

/* qt_hosting - This example hosts a Datoviz Vulkan scene in live Qt Widgets.
 *
 * Qt owns the event loop and widgets. Datoviz creates the Vulkan instance and renders into a
 * Qt-created VkSurfaceKHR through the hosted view contract.
 *
 * Build:  just build
 * Run:    ./build/examples/qt/qt_hosting
 * Smoke:  ./build/examples/qt/qt_hosting --smoke-ms 1000
 * Capture: ./build/examples/qt/qt_hosting --png
 */

#include "hosted_qt_adapter.h"

#include <QApplication>
#include <QCheckBox>
#include <QComboBox>
#include <QCoreApplication>
#include <QDir>
#include <QFont>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
#include <QPixmap>
#include <QPushButton>
#include <QScreen>
#include <QSlider>
#include <QString>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>

#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <vector>



namespace
{
constexpr int EXAMPLE_WINDOW_WIDTH = 1280;
constexpr int EXAMPLE_WINDOW_HEIGHT = 720;
} // namespace



static const uint32_t POINT_COUNT = 96;



static QString _capture_path()
{
    const QString directory = qEnvironmentVariable("DVZ_CAPTURE_DIR", QStringLiteral("."));
    const QString basename =
        qEnvironmentVariable("DVZ_CAPTURE_BASENAME", QStringLiteral("qt_hosting"));
    return QDir(directory).filePath(basename + QStringLiteral(".png"));
}



static bool _capture_window(QWidget* widget)
{
    QScreen* screen = widget->screen();
    if (screen == nullptr)
        return false;

    const QPixmap pixmap = screen->grabWindow(widget->winId());
    if (pixmap.isNull())
        return false;

    const QImage image = pixmap.toImage().scaled(
        EXAMPLE_WINDOW_WIDTH, EXAMPLE_WINDOW_HEIGHT, Qt::IgnoreAspectRatio,
        Qt::SmoothTransformation);
    return image.save(_capture_path(), "PNG");
}



struct SceneState
{
    DvzScene* scene = nullptr;
    DvzFigure* figure = nullptr;
    DvzPanel* panel = nullptr;
    DvzVisual* visual = nullptr;
    vec3 positions[POINT_COUNT] = {};
    DvzColor colors[POINT_COUNT] = {};
    float sizes[POINT_COUNT] = {};
    float point_size = 18.0f;
    float phase = 0.0f;
};



static void _fill_scene_data(SceneState* state)
{
    for (uint32_t i = 0; i < POINT_COUNT; i++)
    {
        const float t = (float)i / (float)POINT_COUNT;
        const float angle = t * 6.2831853f * 3.0f;
        const float radius = 0.18f + 0.62f * t;
        state->positions[i][0] = radius * std::cos(angle);
        state->positions[i][1] = radius * std::sin(angle);
        state->positions[i][2] = 0.0f;
        state->colors[i] = dvz_color_rgb(
            (uint8_t)(80 + (i * 97) % 176), (uint8_t)(80 + (i * 53) % 176),
            (uint8_t)(80 + (i * 29) % 176));
        state->sizes[i] = state->point_size;
    }
}



static int _apply_point_size(SceneState* state, float point_size)
{
    state->point_size = point_size;
    for (uint32_t i = 0; i < POINT_COUNT; i++)
        state->sizes[i] = point_size;
    return dvz_visual_set_data(state->visual, "diameter_px", state->sizes, POINT_COUNT);
}



static int _apply_palette(SceneState* state, int palette)
{
    for (uint32_t i = 0; i < POINT_COUNT; i++)
    {
        const uint8_t a = (uint8_t)((i * 37) % 256);
        const uint8_t b = (uint8_t)((i * 67) % 256);
        if (palette == 0)
        {
            state->colors[i] = dvz_color_rgb(
                (uint8_t)(40 + (a % 180)), (uint8_t)(90 + (b % 130)),
                (uint8_t)(160 + ((a + b) % 80)));
        }
        else if (palette == 1)
        {
            state->colors[i] = dvz_color_rgb(
                (uint8_t)(180 + (a % 70)), (uint8_t)(70 + (b % 120)),
                (uint8_t)(80 + ((a + b) % 90)));
        }
        else
        {
            state->colors[i] = dvz_color_rgb(
                (uint8_t)(70 + (b % 110)), (uint8_t)(170 + (a % 70)),
                (uint8_t)(110 + ((a + b) % 120)));
        }
    }
    return dvz_visual_set_data(state->visual, "color", state->colors, POINT_COUNT);
}



static int _apply_animation_step(SceneState* state)
{
    state->phase += 0.035f;
    for (uint32_t i = 0; i < POINT_COUNT; i++)
    {
        const float t = (float)i / (float)POINT_COUNT;
        const float angle = t * 6.2831853f * 3.0f + state->phase;
        const float wave = 0.08f * std::sin(state->phase * 2.0f + t * 18.0f);
        const float radius = 0.18f + 0.62f * t + wave;
        state->positions[i][0] = radius * std::cos(angle);
        state->positions[i][1] = radius * std::sin(angle);
    }
    return dvz_visual_set_data(state->visual, "position", state->positions, POINT_COUNT);
}



static void _set_background(SceneState* state, int background)
{
    if (background == 0)
        dvz_panel_set_background_color(state->panel, dvz_color_from_unit(0.08f, 0.10f, 0.14f, 1.0f));
    else if (background == 1)
        dvz_panel_set_background_color(state->panel, dvz_color_from_unit(0.94f, 0.95f, 0.92f, 1.0f));
    else
        dvz_panel_set_background_color(state->panel, dvz_color_from_unit(0.03f, 0.11f, 0.10f, 1.0f));
}



static DvzScene* _make_scene(SceneState* state)
{
    DvzScene* scene = dvz_scene();
    if (scene == nullptr)
        return nullptr;

    DvzFigure* figure = dvz_figure(scene, EXAMPLE_WINDOW_WIDTH, EXAMPLE_WINDOW_HEIGHT, 0);
    DvzPanelDesc panel_desc = {};
    panel_desc.x = 0.0f;
    panel_desc.y = 0.0f;
    panel_desc.width = 1.0f;
    panel_desc.height = 1.0f;
    DvzPanel* panel = figure != nullptr ? dvz_panel(figure, &panel_desc) : nullptr;
    DvzVisual* visual = panel != nullptr ? dvz_point(scene, 0) : nullptr;
    if (figure == nullptr || panel == nullptr || visual == nullptr)
    {
        dvz_scene_destroy(scene);
        return nullptr;
    }

    state->scene = scene;
    state->figure = figure;
    state->panel = panel;
    state->visual = visual;
    _fill_scene_data(state);

    dvz_visual_set_data(visual, "position", state->positions, POINT_COUNT);
    dvz_visual_set_data(visual, "color", state->colors, POINT_COUNT);
    dvz_visual_set_data(visual, "diameter_px", state->sizes, POINT_COUNT);
    dvz_panel_add_visual(panel, visual, nullptr);
    _set_background(state, 0);
    return scene;
}



static QWidget* _controls_widget(SceneState* state, DvzQtHostedWindow* view_window, QTimer* timer)
{
    QWidget* controls = new QWidget();
    controls->setMinimumWidth(260);

    QVBoxLayout* layout = new QVBoxLayout(controls);
    layout->setContentsMargins(12, 12, 12, 12);
    layout->setSpacing(10);

    QLabel* title = new QLabel(QStringLiteral("Scene controls"));
    QFont title_font = title->font();
    title_font.setBold(true);
    title->setFont(title_font);
    layout->addWidget(title);

    QLabel* size_label = new QLabel(QStringLiteral("Point size"));
    QSlider* size_slider = new QSlider(Qt::Horizontal);
    size_slider->setRange(4, 48);
    size_slider->setValue((int)state->point_size);
    layout->addWidget(size_label);
    layout->addWidget(size_slider);

    QLabel* wheel_label = new QLabel(QStringLiteral("Wheel sensitivity 100%"));
    QSlider* wheel_slider = new QSlider(Qt::Horizontal);
    wheel_slider->setRange(10, 200);
    wheel_slider->setValue(100);
    layout->addWidget(wheel_label);
    layout->addWidget(wheel_slider);

    QLabel* palette_label = new QLabel(QStringLiteral("Palette"));
    QComboBox* palette_combo = new QComboBox();
    palette_combo->addItem(QStringLiteral("Cool"));
    palette_combo->addItem(QStringLiteral("Warm"));
    palette_combo->addItem(QStringLiteral("Green"));
    layout->addWidget(palette_label);
    layout->addWidget(palette_combo);

    QLabel* background_label = new QLabel(QStringLiteral("Background"));
    QComboBox* background_combo = new QComboBox();
    background_combo->addItem(QStringLiteral("Dark"));
    background_combo->addItem(QStringLiteral("Light"));
    background_combo->addItem(QStringLiteral("Deep green"));
    layout->addWidget(background_label);
    layout->addWidget(background_combo);

    QCheckBox* animate_check = new QCheckBox(QStringLiteral("Animate"));
    QPushButton* reset_button = new QPushButton(QStringLiteral("Reset positions"));
    layout->addWidget(animate_check);
    layout->addWidget(reset_button);
    layout->addStretch(1);

    QObject::connect(size_slider, &QSlider::valueChanged, controls, [state, view_window](int value) {
        (void)_apply_point_size(state, (float)value);
        view_window->request_scene_frame();
    });
    QObject::connect(
        wheel_slider, &QSlider::valueChanged, controls,
        [view_window, wheel_label](int value) {
            view_window->set_wheel_scale((float)value / 100.0f);
            wheel_label->setText(QStringLiteral("Wheel sensitivity %1%").arg(value));
        });
    QObject::connect(
        palette_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), controls,
        [state, view_window](int index) {
            (void)_apply_palette(state, index);
            view_window->request_scene_frame();
        });
    QObject::connect(
        background_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), controls,
        [state, view_window](int index) {
            _set_background(state, index);
            view_window->request_scene_frame();
        });
    QObject::connect(animate_check, &QCheckBox::toggled, controls, [timer](bool checked) {
        if (checked)
            timer->start(16);
        else
            timer->stop();
    });
    QObject::connect(reset_button, &QPushButton::clicked, controls, [state, view_window]() {
        state->phase = 0.0f;
        _fill_scene_data(state);
        (void)dvz_visual_set_data(state->visual, "position", state->positions, POINT_COUNT);
        (void)dvz_visual_set_data(state->visual, "diameter_px", state->sizes, POINT_COUNT);
        view_window->request_scene_frame();
    });
    QObject::connect(timer, &QTimer::timeout, controls, [state, view_window]() {
        (void)_apply_animation_step(state);
        view_window->request_scene_frame();
    });

    return controls;
}



int main(int argc, char** argv)
{
    int smoke_ms = 0;
    bool capture_png = false;
    for (int i = 1; i < argc; i++)
    {
        const QString arg = QString::fromUtf8(argv[i]);
        if (arg == QStringLiteral("--png"))
            capture_png = true;
        else if (arg == QStringLiteral("--smoke-ms") && i + 1 < argc)
            smoke_ms = std::atoi(argv[i + 1]);
    }

    QApplication qt_app(argc, argv);

    std::vector<const char*> extensions;
    if (!dvz_qt_instance_extensions(qt_app, &extensions, "qt_hosting"))
        return 0;

    SceneState scene_state = {};
    DvzScene* scene = _make_scene(&scene_state);
    if (scene == nullptr)
    {
        std::fprintf(stderr, "qt_hosting: failed to create scene\n");
        return 1;
    }

    DvzAppConfig app_cfg = dvz_app_config();
    app_cfg.instance_extension_count = (uint32_t)extensions.size();
    app_cfg.instance_extensions = extensions.data();
    app_cfg.enable_canvas_extensions = true;
    app_cfg.enable_glfw_extensions = false;
    DvzApp* app = dvz_app_with_config(scene, &app_cfg);
    if (app == nullptr)
    {
        std::fprintf(stderr, "qt_hosting: skipped, Datoviz GPU context creation failed\n");
        dvz_scene_destroy(scene);
        return 0;
    }

    VkInstance instance = dvz_app_vk_instance(app);
    if (instance == VK_NULL_HANDLE)
    {
        std::fprintf(stderr, "qt_hosting: Datoviz returned no Vulkan instance\n");
        dvz_app_destroy(app);
        dvz_scene_destroy(scene);
        return 1;
    }

    QVulkanInstance qt_instance;
    qt_instance.setVkInstance(instance);
    if (!qt_instance.create())
    {
        std::fprintf(
            stderr, "qt_hosting: Qt failed to adopt the Datoviz Vulkan instance (%d)\n",
            (int)qt_instance.errorCode());
        dvz_app_destroy(app);
        dvz_scene_destroy(scene);
        return 0;
    }

    DvzQtHostedWindow* view_window = new DvzQtHostedWindow(
        app, scene_state.figure, scene_state.panel, &qt_instance,
        QStringLiteral("qt_hosting_view"),
        QSize(EXAMPLE_WINDOW_WIDTH, EXAMPLE_WINDOW_HEIGHT));
    QWidget* view_container = QWidget::createWindowContainer(view_window);
    view_container->setMinimumSize(640, 480);
    view_container->setFocusPolicy(Qt::StrongFocus);

    QTimer timer;
    QWidget* controls = _controls_widget(&scene_state, view_window, &timer);

    QWidget main_widget;
    main_widget.setWindowTitle(QStringLiteral("Datoviz hosted Qt Widgets"));
    QHBoxLayout* root_layout = new QHBoxLayout(&main_widget);
    root_layout->setContentsMargins(0, 0, 0, 0);
    root_layout->setSpacing(0);
    root_layout->addWidget(view_container, 1);
    root_layout->addWidget(controls, 0);
    main_widget.resize(EXAMPLE_WINDOW_WIDTH, EXAMPLE_WINDOW_HEIGHT);
    main_widget.show();
    bool capture_failed = false;
    QObject::connect(&qt_app, &QCoreApplication::aboutToQuit, &qt_app, [&view_window]() {
        view_window->release_surface();
    });
    if (smoke_ms > 0)
        QTimer::singleShot(smoke_ms, &qt_app, &QCoreApplication::quit);
    if (capture_png)
    {
        QTimer::singleShot(1000, &qt_app, [&main_widget, &capture_failed]() {
            if (!_capture_window(&main_widget))
            {
                std::fprintf(stderr, "qt_hosting: failed to capture the Qt window\n");
                capture_failed = true;
            }
            QCoreApplication::quit();
        });
    }

    const int rc = qt_app.exec();
    timer.stop();
    view_window->release_surface();
    qt_instance.destroy();
    dvz_app_destroy(app);
    dvz_scene_destroy(scene);
    return capture_failed ? 1 : rc;
}
#!/usr/bin/env python3
"""Embed a Datoviz view in a normal PyQt6 Widgets application."""

from __future__ import annotations

import argparse
import ctypes
import random
import sys

from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QMainWindow, QPushButton
from PyQt6.QtWidgets import QSlider, QVBoxLayout, QWidget

import datoviz.raw as dvz
from datoviz.qt import DatovizWidget


def _void_p(array: ctypes.Array) -> ctypes.c_void_p:
    return ctypes.cast(array, ctypes.c_void_p)


class ExampleScene:
    def __init__(self):
        self.scene = dvz.dvz_scene()
        if not self.scene:
            raise RuntimeError('dvz_scene() failed')

        self.figure = dvz.dvz_figure(self.scene, 800, 600, 0)
        self.panel = dvz.dvz_panel_full(self.figure)
        self.visual = dvz.dvz_point(self.scene, 0)
        if not self.figure or not self.panel or not self.visual:
            raise RuntimeError('Datoviz scene setup failed')

        self.positions = (ctypes.c_float * 9)(
            -0.55,
            -0.45,
            0.0,
            +0.55,
            -0.45,
            0.0,
            0.0,
            +0.50,
            0.0,
        )
        self.colors = (dvz.DvzColor * 3)(
            dvz.DvzColor(255, 80, 80, 255),
            dvz.DvzColor(80, 220, 120, 255),
            dvz.DvzColor(90, 150, 255, 255),
        )
        self.diameters = (ctypes.c_float * 3)(24.0, 24.0, 24.0)

        self._upload()
        if dvz.dvz_panel_add_visual(self.panel, self.visual, None) != 0:
            raise RuntimeError('dvz_panel_add_visual() failed')
        dvz.dvz_panel_set_background_color(self.panel, dvz.DvzColor(13, 15, 20, 255))

        controller = dvz.dvz_panzoom(self.scene, None)
        if not controller:
            raise RuntimeError('dvz_panzoom() failed')
        if dvz.dvz_panel_bind_controller(
            self.panel, controller, dvz.DvzDimMaskFlag.DVZ_DIM_MASK_XY
        ):
            raise RuntimeError('dvz_panel_bind_controller() failed')

    def destroy(self):
        if self.scene:
            dvz.dvz_scene_destroy(self.scene)
            self.scene = None

    def set_point_size(self, size: int):
        for i in range(3):
            self.diameters[i] = float(size)
        if dvz.dvz_visual_set_data(self.visual, b'diameter_px', _void_p(self.diameters), 3) != 0:
            raise RuntimeError('dvz_visual_set_data(diameter_px) failed')

    def randomize_colors(self):
        for i in range(3):
            self.colors[i] = dvz.DvzColor(
                random.randint(80, 255),
                random.randint(80, 255),
                random.randint(80, 255),
                255,
            )
        if dvz.dvz_visual_set_data(self.visual, b'color', _void_p(self.colors), 3) != 0:
            raise RuntimeError('dvz_visual_set_data(color) failed')

    def _upload(self):
        if dvz.dvz_visual_set_data(self.visual, b'position', _void_p(self.positions), 3) != 0:
            raise RuntimeError('dvz_visual_set_data(position) failed')
        if dvz.dvz_visual_set_data(self.visual, b'color', _void_p(self.colors), 3) != 0:
            raise RuntimeError('dvz_visual_set_data(color) failed')
        if dvz.dvz_visual_set_data(self.visual, b'diameter_px', _void_p(self.diameters), 3) != 0:
            raise RuntimeError('dvz_visual_set_data(diameter_px) failed')


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Datoviz PyQt hosting')
        self.example = ExampleScene()
        self.datoviz = DatovizWidget(self.example.scene, self.example.figure, self)

        point_size = QSlider(Qt.Orientation.Horizontal)
        point_size.setRange(8, 64)
        point_size.setValue(24)
        point_size.valueChanged.connect(self._set_point_size)

        randomize = QPushButton('Randomize colors')
        randomize.clicked.connect(self._randomize_colors)

        controls = QWidget()
        controls_layout = QVBoxLayout(controls)
        controls_layout.addWidget(QLabel('Point size'))
        controls_layout.addWidget(point_size)
        controls_layout.addWidget(randomize)
        controls_layout.addStretch(1)

        root = QWidget()
        layout = QHBoxLayout(root)
        layout.addWidget(self.datoviz, 1)
        layout.addWidget(controls)
        self.setCentralWidget(root)
        self.resize(1000, 650)

    def closeEvent(self, event):
        self.datoviz.release()
        self.example.destroy()
        super().closeEvent(event)

    def _set_point_size(self, value: int):
        self.example.set_point_size(value)
        self.datoviz.request_frame()

    def _randomize_colors(self):
        self.example.randomize_colors()
        self.datoviz.request_frame()


def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        '--smoke-ms',
        type=int,
        default=0,
        help='quit automatically after this many milliseconds',
    )
    return parser.parse_known_args(argv[1:])


def main(argv: list[str] | None = None) -> int:
    argv = sys.argv if argv is None else argv
    args, qt_args = _parse_args(argv)
    app = QApplication([argv[0], *qt_args])
    window = MainWindow()
    window.show()
    if args.smoke_ms > 0:
        QTimer.singleShot(args.smoke_ms, app.quit)
    rc = int(app.exec())
    window.close()
    app.processEvents()
    return rc


if __name__ == '__main__':
    raise SystemExit(main())
Example details
  • ID: advanced_qt_hosting
  • Category: advanced
  • Lane: advanced
  • Status: supported
  • Source: examples/qt/qt_hosting.cpp
  • Approved adaptation starter: no
  • PyQt source: examples/python/qt/hosted_pyqt.py
  • Browser support: Native only
  • Browser note: Qt and PyQt hosting use native toolkit windows and an optional bridge provider
  • Browser capability tags: native-view, external-surface, qt, optional-provider
  • Validation: optional-native-smoke+screenshot

Tags

advanced, qt, pyqt, optional-provider, external-surface, host-integration

Data

Field Value
kind synthetic