20"""Simple GUIs for individual devices.
22This is meant as a simple GUIs for help during development. It does
23not aim to be pretty; it aims to be simple, complete, and work on any
24OS and Python without extra work. It is not meant as the basis for a
25full-fledged microscope GUI.
27This module requires qtpy which is a requirement for the microscope
28"GUI" extra, i.e., only installed by `pip` if microscope is installed
29with ``pip install microscope[GUI]``.
42from qtpy
import QtCore, QtGui, QtWidgets
47_logger = logging.getLogger(__name__)
52Pyro4.config.SERIALIZERS_ACCEPTED.add(
"pickle")
53Pyro4.config.SERIALIZER =
"pickle"
57 """Table of device settings and its values.
59 This widget simply shows the available settings on the device and
60 their current value. In the future it may add the possibility to
69 layout = QtWidgets.QFormLayout(self)
70 for key, value
in sorted(self.
_device.get_all_settings().items()):
71 layout.addRow(key, QtWidgets.QLabel(parent=self, text=str(value)))
72 self.setLayout(layout)
76 """Show devices in a controller.
78 This widget shows a series of buttons with the name of the
79 multiple devices in a controller. Toggling those buttons displays
80 or hides a widget for that controlled device.
91 QtWidgets.QPushButton, typing.Optional[QtWidgets.QMainWindow]
97 for name
in self.
_device.devices.keys():
98 button = QtWidgets.QPushButton(name, parent=self)
99 button.setCheckable(
True)
105 layout = QtWidgets.QVBoxLayout()
107 layout.addWidget(button)
108 self.setLayout(layout)
111 self, button: QtWidgets.QAbstractButton, checked: bool
116 widget = widget_cls(device)
124 "unchecking subdevice button but there's no window"
133 def put(self, *args, **kwargs):
134 return super().
put(*args, **kwargs)
138 """Helper for CameraWidget handling the internals of the camera trigger."""
140 imageAcquired = QtCore.Signal(numpy.ndarray)
146 if isinstance(self.
_camera, Pyro4.Proxy):
147 pyro_daemon = Pyro4.Daemon()
149 self.
_camera.set_client(queue_uri)
150 data_thread = threading.Thread(
151 target=pyro_daemon.requestLoop, daemon=
True
156 fetch_thread = threading.Thread(target=self.
fetchLoop, daemon=
True)
162 self.destroyed.connect(
lambda: self.
_camera.set_client(
None))
189 self.
_view = QtWidgets.QLabel(parent=self)
191 numpy.zeros(self.
_device.get_sensor_shape(), dtype=numpy.uint8)
207 layout = QtWidgets.QVBoxLayout()
208 controls_row = QtWidgets.QHBoxLayout()
214 controls_row.addWidget(widget)
215 layout.addLayout(controls_row)
216 layout.addWidget(self.
_view)
217 self.setLayout(layout)
220 """Update UI and camera state after enable check box"""
229 "failed to %s camera",
238 numpy.dtype(
"uint8"): QtGui.QImage.Format_Grayscale8,
239 numpy.dtype(
"uint16"): QtGui.QImage.Format_Grayscale16,
241 qt_img = QtGui.QImage(
242 data.tobytes(), *data.shape, np_to_qt[data.dtype]
244 self.
_view.setPixmap(QtGui.QPixmap.fromImage(qt_img))
248 """Display a slider for each actuator.
250 Constructing this widget will set all actuators to their mid-point
251 since the actuators position are not queryable.. The reset button
252 does this too, i.e., it sets all actuators to their mid-point.
263 for i
in range(self.
_device.n_actuators):
264 actuator = QtWidgets.QSlider(QtCore.Qt.Horizontal, parent=self)
265 actuator.setMinimum(0)
266 actuator.setMaximum(100)
268 def setThisActuator(value, actuator_index=i):
271 actuator.valueChanged.connect(setThisActuator)
280 main_layout = QtWidgets.QVBoxLayout()
283 actuators_layout = QtWidgets.QFormLayout()
284 actuators_layout.setLabelAlignment(QtCore.Qt.AlignRight)
285 for i, actuator
in enumerate(self.
_actuators, start=1):
286 actuators_layout.addRow(str(i), actuator)
287 main_layout.addLayout(actuators_layout)
289 self.setLayout(main_layout)
292 if not (0 < actuator_index < self.
_pattern.size):
294 "index %d is invalid because DM has %d actuators"
295 % (actuator_index, self.
_pattern.size)
297 self.
_pattern[actuator_index] = value / 100.0
301 """Set all actuators to their mid-point (0.5)."""
304 for i, actuator
in enumerate(self.
_actuators):
305 actuator.blockSignals(
True)
306 actuator.setSliderPosition(int(self.
_pattern[i] * 100))
307 actuator.blockSignals(
False)
311 """Group of toggle push buttons to change filter position.
313 This widget shows a table of toggle buttons with the filterwheel
324 for i
in range(self.
_device.n_positions):
325 button = QtWidgets.QPushButton(str(i + 1), parent=self)
326 button.setCheckable(
True)
336 layout = QtWidgets.QVBoxLayout()
338 layout.addWidget(button)
339 self.setLayout(layout)
362 lambda x: setattr(self.
_device,
"power", x)
366 str(self.
_device.power), parent=self
377 layout = QtWidgets.QVBoxLayout()
379 power_layout = QtWidgets.QFormLayout()
382 layout.addLayout(power_layout)
383 self.setLayout(layout)
386 """Update UI and light source state after enable check box"""
392 device_is_enabled = self.
_device.get_is_enabled()
397 "failed to %s light",
402 if device_is_enabled:
413 """Stage widget displaying each of the axis position.
415 This widget shows each of the axis, their limits, and a spin box
416 to change the axis position. This requires the stage to be
417 enabled since otherwise it is not able to move it or query the
425 layout = QtWidgets.QFormLayout(self)
426 for name, axis
in self.
_device.axes.items():
427 label =
"%s (%s : %s)" % (
433 position_box = QtWidgets.QDoubleSpinBox(parent=self)
434 position_box.setMinimum(axis.limits.lower)
435 position_box.setMaximum(axis.limits.upper)
436 position_box.setValue(axis.position)
437 position_box.setSingleStep(1.0)
439 def setPositionSlot(position: float, name: str = name):
442 position_box.valueChanged.connect(setPositionSlot)
444 layout.addRow(label, position_box)
445 self.setLayout(layout)
448 self.
_device.axes[name].move_to(position)
452 def __init__(self, widget: QtWidgets.QWidget, *args, **kwargs) ->
None:
455 scroll = QtWidgets.QScrollArea()
456 scroll.setWidgetResizable(
True)
457 scroll.setWidget(widget)
459 self.setCentralWidget(scroll)
461 (QtGui.QKeySequence.Quit, self.
close),
462 (QtGui.QKeySequence.Close, self.
close),
464 shortcut = QtWidgets.QShortcut(key, self)
465 shortcut.activated.connect(slot)
469 if hasattr(device,
"axes"):
471 elif hasattr(device,
"devices"):
472 return ControllerWidget
473 elif hasattr(device,
"n_positions"):
474 return FilterWheelWidget
475 elif hasattr(device,
"power"):
476 return LightSourceWidget
477 elif hasattr(device,
"n_actuators"):
478 return DeformableMirrorWidget
479 elif hasattr(device,
"get_sensor_shape"):
481 elif hasattr(device,
"get_all_settings"):
482 return DeviceSettingsWidget
484 raise TypeError(
"device is not a Microscope Device")
487def main(argv: typing.Sequence[str]) -> int:
488 app = QtWidgets.QApplication(argv)
489 app.setApplicationName(
"Microscope GUI")
490 app.setOrganizationDomain(
"python-microscope.org")
493 "Camera": CameraWidget,
494 "Controller": ControllerWidget,
495 "DeformableMirror": DeformableMirrorWidget,
496 "DeviceSettings": DeviceSettingsWidget,
497 "FilterWheel": FilterWheelWidget,
498 "LightSourceWidget": LightSourceWidget,
499 "Stage": StageWidget,
502 parser = argparse.ArgumentParser(prog=
"microscope-gui")
515 metavar=
"DEVICE-TYPE",
516 choices=type_to_widget.keys(),
517 help=
"Type of device/widget to show",
524 metavar=
"DEVICE-URI",
525 help=
"URI for device",
527 args = parser.parse_args(app.arguments()[1:])
529 device = Pyro4.Proxy(args.uri)
530 widget_cls = type_to_widget[args.type]
531 widget = widget_cls(device)
546 return main(sys.argv)
549if __name__ ==
"__main__":
550 sys.exit(
main(sys.argv))
put(self, *args, **kwargs)
None __init__(self, microscope.abc.Camera camera)
None __init__(self, QtWidgets.QWidget widget, *args, **kwargs)
int _setuptools_entry_point()
int main(typing.Sequence[str] argv)
QtWidgets.QWidget _guess_device_widget(device)