Qt Integration¶
pymmcore-plus is designed to integrate with Qt GUIs. (For complete Qt applications
built on it, see pymmcore-gui, and the widgets in
pymmcore-widgets.) It accomplishes this in
two ways:
CMMCorePlus.run_mdaruns in a thread in order to not block the event loop.- The
eventsobjects (core.eventsandcore.mda.events) use Qt signals instead of signals from the psygnal library when a Qt application is running. A Qt signal emitted in another thread is delivered in the main thread (to callbacks connected from the main thread), which keeps things from crashing when your callbacks update the GUI.
The examples on this page require qtpy, superqt and a Qt backend installed in the
environment (all of which come with pip install "pymmcore-widgets[PyQt6]"):
Create the QApplication first¶
Whether CMMCorePlus uses Qt signals or psygnal is decided once, when the core is
created. It uses Qt signals if a QApplication exists at that moment, and psygnal
otherwise. So create the QApplication before the CMMCorePlus (and that includes the first
call to CMMCorePlus.instance(), which is what creates the global core):
from pymmcore_plus import CMMCorePlus
from qtpy.QtWidgets import QApplication
app = QApplication([])
core = CMMCorePlus.instance()
print(type(core.events).__name__) # QCoreSignaler
If the core comes first, core.events is a psygnal CMMCoreSignaler, and the
callbacks connected to core.mda.events (frameReady, sequenceFinished, etc...)
run in the acquisition thread. Qt widgets may only be touched from the main thread,
so updating the GUI from one of those callbacks can crash the application, or make it
misbehave in less obvious ways. (The events guide has more on
which thread runs your callbacks.)
You can also choose the backend with the PYMM_SIGNALS_BACKEND environment variable
(qt, psygnal or auto; see Environment variables).
Importing pymmcore_widgets sets it to qt (unless you've set it yourself), so in a
GUI built from the widgets the order matters less. But creating the application
first is a good habit.
Avoiding blocking the Qt event loop¶
If you make a blocking call on the thread running the Qt event loop then your GUI will
become unresponsive. The recommended way to avoid this is to use threads to call
blocking methods like snapImage yourself, and
let pymmcore-plus handle the threading when you use
run_mda.
The simple application below consists of a counter that increments so long as the
event loop is not blocked, and two buttons that call the snapImage method (with a
5 second exposure). One button calls it from a thread, and the counter continues; the
other blocks, and the counter stops. Things to notice in the code below:
- Create the
QApplicationbefore the core. - Use
CMMCorePlus.instance()to create the core object. This allows another script (or widget) in the same process to use the same object. - Use a thread for blocking operations like
snapImage.
import sys
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
from superqt.utils import create_worker
from pymmcore_plus import CMMCorePlus
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.mmc = CMMCorePlus.instance()
self.mmc.loadSystemConfiguration()
self.mmc.setExposure(5000) # 5 seconds
self.counter = 0
layout = QVBoxLayout()
self.l = QLabel("Start")
b_blocking = QPushButton("Snap blocking")
b_threaded = QPushButton("Snap threaded")
b_blocking.pressed.connect(self.snap_blocking)
b_threaded.pressed.connect(self.snap_threaded)
layout.addWidget(self.l)
layout.addWidget(b_blocking)
layout.addWidget(b_threaded)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self._recurring_timer)
self.timer.start()
def _recurring_timer(self):
self.counter += 1
self.l.setText("Counter: %d" % self.counter)
def snap_threaded(self):
# alternatively you could use the python threading module
# or directly use QThreads
create_worker(
self.mmc.snapImage,
_start_thread=True,
)
def snap_blocking(self):
self.mmc.snapImage()
app = QApplication(sys.argv) # the app first...
window = MainWindow() # ... then the core (created in MainWindow.__init__)
app.exec()
Running an acquisition from a Qt application¶
run_mda starts the acquisition in another
thread and returns immediately, so you can call it right before starting the event
loop (or from a button). Because the QApplication exists before the core, the
frameReady callback below runs in the main thread, where it would be safe to update
a widget with the new image. The STOP button cancels the acquisition:
"""
An example of integrating a MMCore object into an event loop.
This example requires qtpy and a Qt backend installed in the env:
"""
import numpy as np
from qtpy.QtWidgets import QApplication, QPushButton
from useq import MDAEvent, MDASequence
from pymmcore_plus import CMMCorePlus
# create the QApplication *before* the core, so that the core uses Qt signals
app = QApplication([])
mmcore = CMMCorePlus.instance()
# button for early stopping
stop = QPushButton("STOP")
def stop_clicked():
mmcore.mda.cancel()
app.quit()
stop.clicked.connect(stop_clicked)
stop.show()
@mmcore.mda.events.frameReady.connect
def on_frame(image: np.ndarray, event: MDAEvent):
# with Qt signals, this runs in the main thread (not the acquisition thread),
# so it's safe to update widgets from here
print(
f"received frame: {image.shape}, {image.dtype} "
f"@ index {event.index}, z={event.z_pos}"
)
@mmcore.events.propertyChanged.connect
def prop_changed(device, prop, value):
print(f"{device}.{prop} changed to {value!r}")
# setup some callbacks
mmcore.events.systemConfigurationLoaded.connect(lambda: print("config loaded!"))
mmcore.mda.events.sequenceFinished.connect(app.quit)
# load config and start an experiment
mmcore.loadSystemConfiguration()
# see https://github.com/pymmcore-plus/useq-schema
x, y = mmcore.getXYPosition()
sequence = MDASequence(
channels=["DAPI", {"config": "FITC", "exposure": 50}],
time_plan={"interval": 1.5, "loops": 5},
# a relative z plan is relative to the stage position, so give it the current one
stage_positions=[(x, y, mmcore.getZPosition())],
z_plan={"range": 4, "step": 0.5},
axis_order="tpcz",
)
mmcore.run_mda(sequence) # runs in another thread, and returns immediately
# start the qt event loop
app.exec()