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__)
50# We use pickle so we can serialize numpy arrays for camera images and
51# deformable mirrors patterns.
52Pyro4.config.SERIALIZERS_ACCEPTED.add("pickle")
53Pyro4.config.SERIALIZER = "pickle"
56class DeviceSettingsWidget(QtWidgets.QWidget):
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
65 def __init__(self, device: microscope.
abc.Device, *args, **kwargs) ->
None:
66 super().__init__(*args, **kwargs)
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.
87 super().__init__(*args, **kwargs)
90 self._button2window: typing.Dict[
91 QtWidgets.QPushButton, typing.Optional[QtWidgets.QMainWindow]
93 self._button2name: typing.Dict[QtWidgets.QPushButton, str] = {}
97 for name
in self.
_device.devices.keys():
98 button = QtWidgets.QPushButton(name, parent=self)
99 button.setCheckable(
True)
101 self._button2name[button] = name
102 self._button2window[button] =
None
105 layout = QtWidgets.QVBoxLayout()
107 layout.addWidget(button)
108 self.setLayout(layout)
110 def toggleDeviceWidget(
111 self, button: QtWidgets.QAbstractButton, checked: bool
114 device = self.
_device.devices[self._button2name[button]]
115 widget_cls = _guess_device_widget(device)
116 widget = widget_cls(device)
118 self._button2window[button] = window
121 window = self._button2window[button]
124 "unchecking subdevice button but there's no window"
128 self._button2window[button] =
None
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)
142 def __init__(self, camera: microscope.
abc.Camera) ->
None:
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))
164 def snap(self) -> None:
167 def fetchLoop(self) -> None:
183 def __init__(self, device: microscope.
abc.Camera, *args, **kwargs) ->
None:
184 super().__init__(*args, **kwargs)
189 self.
_view = QtWidgets.QLabel(parent=self)
191 numpy.zeros(self.
_device.get_sensor_shape(), dtype=numpy.uint8)
194 self.
_enable_check = QtWidgets.QCheckBox(
"Enabled", parent=self)
202 self.
_snap_button = QtWidgets.QPushButton(
"Snap", parent=self)
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",
236 def displayData(self, data: numpy.ndarray) ->
None:
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.
258 super().__init__(*args, **kwargs)
262 self._actuators: typing.List[QtWidgets.QSlider] = []
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)
272 self._actuators.append(actuator)
277 self.
_reset_button = QtWidgets.QPushButton(
"Reset", parent=self)
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)
291 def setActuatorValue(self, actuator_index: int, value: int) ->
None:
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
320 super().__init__(*args, **kwargs)
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)
341 def setFilterWheelPosition(self) -> None:
349 super().__init__(*args, **kwargs)
352 self.
_enable_check = QtWidgets.QCheckBox(
"Enabled", parent=self)
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:
408 def updateCurrentPower(self) -> None:
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
421 def __init__(self, device: microscope.
abc.Stage, *args, **kwargs) ->
None:
422 super().__init__(*args, **kwargs)
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)
447 def setPosition(self, name: str, position: float) ->
None:
448 self.
_device.axes[name].move_to(position)
452 def __init__(self, widget: QtWidgets.QWidget, *args, **kwargs) ->
None:
453 super().__init__(*args, **kwargs)
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)
468def _guess_device_widget(device) -> QtWidgets.QWidget:
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)
538def _setuptools_entry_point() -> int:
546 return main(sys.argv)
549if __name__ ==
"__main__":
550 sys.exit(main(sys.argv))