Programming Guidelines for Python GUIs

Overview

This section explains how to build custom Python graphical user interfaces using the provided DDT widgets. This sections shows concrete code fragments that you can reuse directly when writing your own applications.

Using Qt Designer with DDT Widgets

When editing .ui files you can add the DDT widgets directly in Qt Designer. Ensure Qt can find the DDT plugins and libraries before launching Designer:

export QT_PLUGIN_PATH=/elt/ddt/lib64:$QT_PLUGIN_PATH
export LD_LIBRARY_PATH=/elt/ddt/lib64/designer:$LD_LIBRARY_PATH

Adjust the paths to the location of your DDT installation. With these variables set, Qt Designer will load the DDT widget plugin so the widgets are available in the designer palette.

You can use the DDT widgets as the other Qt Widgets.

Sample DDT GUIs

The examples show in this chapter are based on the DDT applications and on the DDT examples (ddt.examples repository: https://gitlab.eso.org/ifw/ddt.examples.git)

Imports and Initialization

The essential imports and startup steps are the same for all GUIs:

import ddtImageHandling
import ddtWidgets
from DdtUtils import DdtLogger
from utils.pyDdtImageWidget import PyDdtImageWidget

Choose a base class:

  • Derive from ``PyDdtViewer`` if you want to reuse the standard viewer, this is

the best option. Example:

from pyDdtViewer.viewer import PyDdtViewer
class MainWindow(PyDdtViewer):
    ...
  • Wrap ``PyDdtImageWidget`` in your own ``QMainWindow`` if you want a lean window around the image widget. Example (from the streaming view in ddtex2):

class DdtEx2(QMainWindow):
    def __init__(...):
        super().__init__()
        self.ui = Ui_DdtEx2()
        self.ui.setupUi(self)
        self.py_image_widget = PyDdtImageWidget(self.ui.ddtImageWidget, logger)

Utility Class

The PyDdtImageWidget is a utility class provided by DDT to simplify the creation process of a DdtImageWidget specially for the creation of dialogs and connections between the widgets.

Wiring Signals and Data Streams

The way to interact between DDT widgets is using the Qt signals and slots. For instance, the DDT Image Widget exposes slots for attaching and detaching data streams.

You can have a look to the signals/slot provided by the Image Widget here: Image widget signals and slots

Define Qt signals in your window and connect them to the widget slots:

from PySide6.QtCore import Signal

class DdtEx2(QMainWindow):
    attach_data_stream_signal = Signal(str)
    detach_stream_signal = Signal(None)

    def __init__(self, localbroker_uri, datastream, debug=False):
        ...
        self.attach_data_stream_signal.connect(self.ui.ddtImageWidget.AttachDataStream)
        self.detach_stream_signal.connect(self.ui.ddtImageWidget.DetachStream)

        if localbroker_uri and datastream:
            full_uri = localbroker_uri + " " + datastream
            self.attach_data_stream_signal.emit(full_uri)

Adding Custom Interaction

Drawing overlays

You can use ddtWidgets primitives to add graphical overlays on top of the image:

prop = ddtWidgets.DdtGraphicalElementProperties()
rect = ddtWidgets.DdtGraphicalElementRectangle(prop, x0, y0, width, height)
text = ddtWidgets.DdtGraphicalElementText(prop, x0, y0, str(label))
cross_prop = ddtWidgets.DdtGraphicalElementProperties()
cross_prop.line_colour.setRgb(0, 255, 0)
cross = ddtWidgets.DdtGraphicalElementCross(cross_prop, center_x, center_y, 10)

overlay = self.ui.ddtImageWidget.get_graphical_overlay()
overlay.AddGraphicalElement(rect)
overlay.AddGraphicalElement(text)
overlay.AddGraphicalElement(cross)
self.ui.ddtImageWidget.RedrawOverlay()

Processing Images from ddtImageWidget

ddtEx3 example demonstrates how to process the image currently displayed in the ddtImageWidget using an external pybind11-based library. See ddtEx3 for the full working example.

The widget fires a NewBoostDataEvent Qt signal every time a new image arrives. This is a Shiboken-wrapped boost signal and is connected like any other Qt signal using .connect():

class DdtEx3(QMainWindow):
    def __init__(self, localbroker_uri, datastream):
        super().__init__()
        self.ui = Ui_DdtEx3()
        self.ui.setupUi(self)
        # ...
        self.ui.ddtImageWidget.NewBoostDataEvent.connect(self.new_data_handler)

    @Slot()
    def new_data_handler(self):
        image_handle = self.ui.ddtImageWidget.GetCurrentlyDisplayedImageAsRawPtr()
        image = pycpl.image(image_handle)  # copies the data

Note

GetCurrentImageCopyAsRawPtr() returns a pointer to a copy of the last received image owned by the simulator. That copy is replaced on the next data arrival, so construct pycpl.image inside the callback before returning.

Image can be also converted to a NumPy array:

Alternatively, CplImageView provides direct buffer access from a raw image pointer, without going through pycpl:

import CplImageView
import numpy

view = CplImageView.CplImageView(image_handle)
# Zero-copy view (shares memory with the cpl_image):
arr_view = numpy.array(view, copy=False)
# Deep copy (owns its own memory, safe after the source image is overwritten):
arr_copy = numpy.array(view, copy=True)

CLI Image Subscription with DdtSubscriber (ddtEx4)

ddtEx4 shows how to receive and process images without a GUI using the DdtSubscriber module. See ddtEx4 for the full working example. For the full API reference and a comparison with the low-level DdtDataSubscriber, see the DdtSubscriber Module section in components.rst.

Key points:

  • DdtSubscriber accepts the same CLI arguments as the standalone ddtSubscriberSimulator binary (-b for broker URI, -s for data stream identifier).

  • RunTransfer() blocks while releasing the GIL; run it in a daemon thread.

  • Register a Python callback with ConnectNewImage(). The pybind11 layer acquires the GIL before calling into Python, so the handler can use Python objects directly.

  • ConnectNewImage() returns a SignalConnection; call .disconnect() before stopping the subscriber to prevent callbacks arriving during teardown.

import DdtSubscriber as DdtSub

class DdtEx4:
    def __init__(self, localbroker_uri, datastream):
        ddtImageHandling.InitCpl()
        self._subscriber = DdtSub.DdtSubscriber()
        self._subscriber.Init(["ddtEx4", "-b", localbroker_uri, "-s", datastream])
        self._connection = self._subscriber.ConnectNewImage(self._new_data_handler)
        threading.Thread(target=self._subscriber.RunTransfer, daemon=True).start()

    def run(self):
        _stop_event.wait()  # block until SIGINT/SIGTERM
        self._connection.disconnect()
        self._subscriber.Stop()

    def _new_data_handler(self):
        # Called from the subscriber thread; GIL is held by pybind11.
        image_handle = self._subscriber.GetCurrentImageCopyAsRawPtr()
        image = pycpl.image(image_handle)  # copies the data

Invoke ddtEx4 with:

ddtEx4 -l zpb.rr://127.0.0.1:5001 -s ds1

Accessing Received Images

Inside the ConnectNewImage callback, call GetCurrentImageCopyAsRawPtr() to obtain a uintptr_t handle to a copy of the last received CPL image.

Note

The copy is replaced on every new data arrival. Construct pycpl.image (or CplImageView) inside the callback before returning.

The handle can be used in several ways:

  1. pycpl image — full copy, safe to keep after the callback returns:

    image = pycpl.image(image_handle)
    arr = numpy.asarray(image)
    
  2. CplImageView — zero-copy buffer access without pycpl:

    import CplImageView
    import numpy
    
    view = CplImageView.CplImageView(image_handle)
    # Zero-copy view (shares memory with the cpl_image):
    arr_view = numpy.array(view, copy=False)
    # Deep copy (owns its own memory, safe after the source image is overwritten):
    arr_copy = numpy.array(view, copy=True)