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)
Recommended Workflow for New GUIs
Design the UI in Qt Designer and add a
wscriptwithdeclare_pyqtprogram.Decide whether to derive from
PyDdtViewer(full viewer) or to start from empty design.In the constructor, set up the generated UI class, initialise the logger, and call
ddtImageHandling.InitCpl()before touching image widgets.Wire signals for file loading, stream attachment, and custom actions. Use
Signalobjects andemitto decouple UI elements from the underlying DDT slots.Add domain logic (overlays, validation, button state handling, etc.) in dedicated helper methods to keep the GUI code readable.
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:
DdtSubscriberaccepts the same CLI arguments as the standaloneddtSubscriberSimulatorbinary (-bfor broker URI,-sfor 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 aSignalConnection; 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:
pycpl image — full copy, safe to keep after the callback returns:
image = pycpl.image(image_handle) arr = numpy.asarray(image)
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)