components.crosshair_graphic_view

  1from PyQt6.QtCore import pyqtSignal, Qt
  2from PyQt6.QtGui import QPainter, QPen, QColor, QMouseEvent, QWheelEvent
  3from PyQt6.QtWidgets import QGraphicsView
  4
  5
  6class CrosshairGraphicsView(QGraphicsView):
  7    """Custom QGraphicsView subclass that supports:
  8       - Live crosshair display following mouse movements
  9       - Coordinate tracking and emission via Qt signals
 10       - Integration with a parent viewer for synchronized slice updates
 11    """
 12
 13    # Signal emitted whenever the mouse moves — sends (view_idx, x, y).
 14    coordinate_changed = pyqtSignal(int, int, int)
 15    """**Signal(int, int, int):** Emitted whenever the mouse moves within a view.  
 16    Parameters represent:
 17    - `view_idx`: The index of the active view (e.g., axial, coronal, sagittal).  
 18    - `x`, `y`: The current mouse coordinates within that view.
 19    """
 20
 21    def __init__(self, view_idx, parent=None):
 22        """
 23        Initialize the crosshair view.
 24
 25        Args:
 26            view_idx (int): Index of the view (e.g. axial=0, coronal=1, sagittal=2)
 27            parent (QWidget): Parent viewer widget (usually manages multiple CrosshairGraphicsViews)
 28        """
 29        super().__init__(parent)
 30        self.view_idx = view_idx
 31        self.parent_viewer = parent
 32
 33        # Enable continuous mouse tracking (even when no button pressed)
 34        self.setMouseTracking(True)
 35
 36        # Disable dragging mode (only used for coordinate interaction)
 37        self.setDragMode(QGraphicsView.DragMode.NoDrag)
 38
 39        # Crosshair graphics items (horizontal and vertical lines)
 40        self.crosshair_h = None
 41        self.crosshair_v = None
 42        self.crosshair_visible = False
 43
 44        # Enable anti-aliasing and smooth scaling for better visual quality
 45        self.setRenderHints(
 46            QPainter.RenderHint.Antialiasing |
 47            QPainter.RenderHint.SmoothPixmapTransform
 48        )
 49
 50    # -------------------------------------------------------------------------
 51    # Crosshair setup and management
 52    # -------------------------------------------------------------------------
 53    def setup_crosshairs(self):
 54        """
 55        Initialize the crosshair lines once the QGraphicsScene is available.
 56        Adds two semi-transparent yellow lines to the scene (horizontal + vertical).
 57        """
 58        if self.scene():
 59            # Yellow, semi-transparent pen for crosshair
 60            pen = QPen(QColor(255, 255, 0, 180), 1)
 61
 62            # Create horizontal and vertical crosshair lines in the scene
 63            self.crosshair_h = self.scene().addLine(0, 0, 0, 0, pen)
 64            self.crosshair_v = self.scene().addLine(0, 0, 0, 0, pen)
 65
 66            # Initially invisible
 67            self.crosshair_h.setVisible(False)
 68            self.crosshair_v.setVisible(False)
 69
 70    # -------------------------------------------------------------------------
 71    # Mouse movement: update coordinates and crosshair position
 72    # -------------------------------------------------------------------------
 73    def mouseMoveEvent(self, event: QMouseEvent):
 74        """
 75        Handle mouse movement:
 76          - Maps cursor position to scene coordinates
 77          - Checks bounds
 78          - Updates crosshair lines
 79          - Emits coordinate_changed signal
 80        """
 81        if self.scene() and self.parent_viewer and self.parent_viewer.img_data is not None:
 82            # Map mouse position from view to scene coordinates
 83            pos = self.mapToScene(event.pos())
 84            x, y = int(pos.x()), int(pos.y())
 85
 86            # Get scene dimensions for bounds checking
 87            scene_rect = self.scene().sceneRect()
 88
 89            # Only process if cursor is inside the image area
 90            if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
 91                # Move crosshair to current position
 92                self.update_crosshairs(x, y)
 93
 94                # Notify parent about the new coordinates
 95                self.coordinate_changed.emit(self.view_idx, x, y)
 96
 97        # Pass event to base class for default behavior
 98        super().mouseMoveEvent(event)
 99
100    # -------------------------------------------------------------------------
101    # Mouse scroll: change slices
102    # -------------------------------------------------------------------------
103
104    def wheelEvent(self, event: QWheelEvent):
105        delta = int(event.angleDelta().y()/120)
106        self.parent_viewer.handle_scroll(self.view_idx, delta)
107        event.accept()
108
109    # -------------------------------------------------------------------------
110    # Mouse click: trigger coordinate selection or slice updates
111    # -------------------------------------------------------------------------
112    def mousePressEvent(self, event: QMouseEvent):
113        """
114        Handle mouse clicks:
115          - On left-click, compute image coordinates
116          - Notify parent viewer for cross-view synchronization or slice update
117        """
118        if (event.button() == Qt.MouseButton.LeftButton and
119                self.scene() and self.parent_viewer and
120                self.parent_viewer.img_data is not None):
121
122            # Convert click position to scene coordinates
123            pos = self.mapToScene(event.pos())
124            x, y = int(pos.x()), int(pos.y())
125
126            # Verify position is inside the scene
127            scene_rect = self.scene().sceneRect()
128            if 0 <= x < scene_rect.width() and 0 <= y < scene_rect.height():
129                # Delegate handling to parent viewer (e.g., update other views)
130                self.parent_viewer.handle_click_coordinates(self.view_idx, x, y)
131
132        super().mousePressEvent(event)
133
134    # -------------------------------------------------------------------------
135    # Update crosshair position visually
136    # -------------------------------------------------------------------------
137    def update_crosshairs(self, x, y):
138        """
139        Move crosshair lines to a new position (x, y).
140        Makes them visible if hidden.
141        """
142        if self.crosshair_h and self.crosshair_v and self.scene():
143            scene_rect = self.scene().sceneRect()
144
145            # Horizontal line across the scene
146            self.crosshair_h.setLine(0, y, scene_rect.width(), y)
147
148            # Vertical line across the scene
149            self.crosshair_v.setLine(x, 0, x, scene_rect.height())
150
151            # If not visible yet, make them appear
152            if not self.crosshair_visible:
153                self.crosshair_h.setVisible(True)
154                self.crosshair_v.setVisible(True)
155                self.crosshair_visible = True
156
157    # -------------------------------------------------------------------------
158    # External setter for crosshair position (used by parent viewer)
159    # -------------------------------------------------------------------------
160    def set_crosshair_position(self, x, y):
161        """
162        Allows external components (e.g. linked views) to set the crosshair.
163        Ensures it's within bounds before updating.
164        """
165        if self.crosshair_h and self.crosshair_v and self.scene():
166            scene_rect = self.scene().sceneRect()
167            if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
168                self.crosshair_h.setLine(0, y, scene_rect.width(), y)
169                self.crosshair_v.setLine(x, 0, x, scene_rect.height())
170                self.crosshair_h.setVisible(True)
171                self.crosshair_v.setVisible(True)
172                self.crosshair_visible = True
173
174    # -------------------------------------------------------------------------
175    # Handle when the mouse leaves the widget area
176    # -------------------------------------------------------------------------
177    def leaveEvent(self, event):
178        """
179        Hide or update crosshairs when the mouse leaves the view area.
180        Currently, it calls the parent viewer’s update function to
181        synchronize crosshairs across multiple views.
182        """
183        if self.crosshair_h and self.crosshair_v:
184            # Previous behavior (hidden when leaving):
185            # self.crosshair_h.setVisible(False)
186            # self.crosshair_v.setVisible(False)
187            # self.crosshair_visible = False
188
189            # New behavior: ask parent to refresh all cross-view lines
190            self.parent_viewer.update_cross_view_lines()
191
192        super().leaveEvent(event)
class CrosshairGraphicsView(PyQt6.QtWidgets.QGraphicsView):
  7class CrosshairGraphicsView(QGraphicsView):
  8    """Custom QGraphicsView subclass that supports:
  9       - Live crosshair display following mouse movements
 10       - Coordinate tracking and emission via Qt signals
 11       - Integration with a parent viewer for synchronized slice updates
 12    """
 13
 14    # Signal emitted whenever the mouse moves — sends (view_idx, x, y).
 15    coordinate_changed = pyqtSignal(int, int, int)
 16    """**Signal(int, int, int):** Emitted whenever the mouse moves within a view.  
 17    Parameters represent:
 18    - `view_idx`: The index of the active view (e.g., axial, coronal, sagittal).  
 19    - `x`, `y`: The current mouse coordinates within that view.
 20    """
 21
 22    def __init__(self, view_idx, parent=None):
 23        """
 24        Initialize the crosshair view.
 25
 26        Args:
 27            view_idx (int): Index of the view (e.g. axial=0, coronal=1, sagittal=2)
 28            parent (QWidget): Parent viewer widget (usually manages multiple CrosshairGraphicsViews)
 29        """
 30        super().__init__(parent)
 31        self.view_idx = view_idx
 32        self.parent_viewer = parent
 33
 34        # Enable continuous mouse tracking (even when no button pressed)
 35        self.setMouseTracking(True)
 36
 37        # Disable dragging mode (only used for coordinate interaction)
 38        self.setDragMode(QGraphicsView.DragMode.NoDrag)
 39
 40        # Crosshair graphics items (horizontal and vertical lines)
 41        self.crosshair_h = None
 42        self.crosshair_v = None
 43        self.crosshair_visible = False
 44
 45        # Enable anti-aliasing and smooth scaling for better visual quality
 46        self.setRenderHints(
 47            QPainter.RenderHint.Antialiasing |
 48            QPainter.RenderHint.SmoothPixmapTransform
 49        )
 50
 51    # -------------------------------------------------------------------------
 52    # Crosshair setup and management
 53    # -------------------------------------------------------------------------
 54    def setup_crosshairs(self):
 55        """
 56        Initialize the crosshair lines once the QGraphicsScene is available.
 57        Adds two semi-transparent yellow lines to the scene (horizontal + vertical).
 58        """
 59        if self.scene():
 60            # Yellow, semi-transparent pen for crosshair
 61            pen = QPen(QColor(255, 255, 0, 180), 1)
 62
 63            # Create horizontal and vertical crosshair lines in the scene
 64            self.crosshair_h = self.scene().addLine(0, 0, 0, 0, pen)
 65            self.crosshair_v = self.scene().addLine(0, 0, 0, 0, pen)
 66
 67            # Initially invisible
 68            self.crosshair_h.setVisible(False)
 69            self.crosshair_v.setVisible(False)
 70
 71    # -------------------------------------------------------------------------
 72    # Mouse movement: update coordinates and crosshair position
 73    # -------------------------------------------------------------------------
 74    def mouseMoveEvent(self, event: QMouseEvent):
 75        """
 76        Handle mouse movement:
 77          - Maps cursor position to scene coordinates
 78          - Checks bounds
 79          - Updates crosshair lines
 80          - Emits coordinate_changed signal
 81        """
 82        if self.scene() and self.parent_viewer and self.parent_viewer.img_data is not None:
 83            # Map mouse position from view to scene coordinates
 84            pos = self.mapToScene(event.pos())
 85            x, y = int(pos.x()), int(pos.y())
 86
 87            # Get scene dimensions for bounds checking
 88            scene_rect = self.scene().sceneRect()
 89
 90            # Only process if cursor is inside the image area
 91            if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
 92                # Move crosshair to current position
 93                self.update_crosshairs(x, y)
 94
 95                # Notify parent about the new coordinates
 96                self.coordinate_changed.emit(self.view_idx, x, y)
 97
 98        # Pass event to base class for default behavior
 99        super().mouseMoveEvent(event)
100
101    # -------------------------------------------------------------------------
102    # Mouse scroll: change slices
103    # -------------------------------------------------------------------------
104
105    def wheelEvent(self, event: QWheelEvent):
106        delta = int(event.angleDelta().y()/120)
107        self.parent_viewer.handle_scroll(self.view_idx, delta)
108        event.accept()
109
110    # -------------------------------------------------------------------------
111    # Mouse click: trigger coordinate selection or slice updates
112    # -------------------------------------------------------------------------
113    def mousePressEvent(self, event: QMouseEvent):
114        """
115        Handle mouse clicks:
116          - On left-click, compute image coordinates
117          - Notify parent viewer for cross-view synchronization or slice update
118        """
119        if (event.button() == Qt.MouseButton.LeftButton and
120                self.scene() and self.parent_viewer and
121                self.parent_viewer.img_data is not None):
122
123            # Convert click position to scene coordinates
124            pos = self.mapToScene(event.pos())
125            x, y = int(pos.x()), int(pos.y())
126
127            # Verify position is inside the scene
128            scene_rect = self.scene().sceneRect()
129            if 0 <= x < scene_rect.width() and 0 <= y < scene_rect.height():
130                # Delegate handling to parent viewer (e.g., update other views)
131                self.parent_viewer.handle_click_coordinates(self.view_idx, x, y)
132
133        super().mousePressEvent(event)
134
135    # -------------------------------------------------------------------------
136    # Update crosshair position visually
137    # -------------------------------------------------------------------------
138    def update_crosshairs(self, x, y):
139        """
140        Move crosshair lines to a new position (x, y).
141        Makes them visible if hidden.
142        """
143        if self.crosshair_h and self.crosshair_v and self.scene():
144            scene_rect = self.scene().sceneRect()
145
146            # Horizontal line across the scene
147            self.crosshair_h.setLine(0, y, scene_rect.width(), y)
148
149            # Vertical line across the scene
150            self.crosshair_v.setLine(x, 0, x, scene_rect.height())
151
152            # If not visible yet, make them appear
153            if not self.crosshair_visible:
154                self.crosshair_h.setVisible(True)
155                self.crosshair_v.setVisible(True)
156                self.crosshair_visible = True
157
158    # -------------------------------------------------------------------------
159    # External setter for crosshair position (used by parent viewer)
160    # -------------------------------------------------------------------------
161    def set_crosshair_position(self, x, y):
162        """
163        Allows external components (e.g. linked views) to set the crosshair.
164        Ensures it's within bounds before updating.
165        """
166        if self.crosshair_h and self.crosshair_v and self.scene():
167            scene_rect = self.scene().sceneRect()
168            if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
169                self.crosshair_h.setLine(0, y, scene_rect.width(), y)
170                self.crosshair_v.setLine(x, 0, x, scene_rect.height())
171                self.crosshair_h.setVisible(True)
172                self.crosshair_v.setVisible(True)
173                self.crosshair_visible = True
174
175    # -------------------------------------------------------------------------
176    # Handle when the mouse leaves the widget area
177    # -------------------------------------------------------------------------
178    def leaveEvent(self, event):
179        """
180        Hide or update crosshairs when the mouse leaves the view area.
181        Currently, it calls the parent viewer’s update function to
182        synchronize crosshairs across multiple views.
183        """
184        if self.crosshair_h and self.crosshair_v:
185            # Previous behavior (hidden when leaving):
186            # self.crosshair_h.setVisible(False)
187            # self.crosshair_v.setVisible(False)
188            # self.crosshair_visible = False
189
190            # New behavior: ask parent to refresh all cross-view lines
191            self.parent_viewer.update_cross_view_lines()
192
193        super().leaveEvent(event)

Custom QGraphicsView subclass that supports:

  • Live crosshair display following mouse movements
  • Coordinate tracking and emission via Qt signals
  • Integration with a parent viewer for synchronized slice updates
CrosshairGraphicsView(view_idx, parent=None)
22    def __init__(self, view_idx, parent=None):
23        """
24        Initialize the crosshair view.
25
26        Args:
27            view_idx (int): Index of the view (e.g. axial=0, coronal=1, sagittal=2)
28            parent (QWidget): Parent viewer widget (usually manages multiple CrosshairGraphicsViews)
29        """
30        super().__init__(parent)
31        self.view_idx = view_idx
32        self.parent_viewer = parent
33
34        # Enable continuous mouse tracking (even when no button pressed)
35        self.setMouseTracking(True)
36
37        # Disable dragging mode (only used for coordinate interaction)
38        self.setDragMode(QGraphicsView.DragMode.NoDrag)
39
40        # Crosshair graphics items (horizontal and vertical lines)
41        self.crosshair_h = None
42        self.crosshair_v = None
43        self.crosshair_visible = False
44
45        # Enable anti-aliasing and smooth scaling for better visual quality
46        self.setRenderHints(
47            QPainter.RenderHint.Antialiasing |
48            QPainter.RenderHint.SmoothPixmapTransform
49        )

Initialize the crosshair view.

Args: view_idx (int): Index of the view (e.g. axial=0, coronal=1, sagittal=2) parent (QWidget): Parent viewer widget (usually manages multiple CrosshairGraphicsViews)

def coordinate_changed(unknown):

Signal(int, int, int): Emitted whenever the mouse moves within a view.
Parameters represent:

  • view_idx: The index of the active view (e.g., axial, coronal, sagittal).
  • x, y: The current mouse coordinates within that view.
view_idx
parent_viewer
crosshair_h
crosshair_v
crosshair_visible
def setup_crosshairs(self):
54    def setup_crosshairs(self):
55        """
56        Initialize the crosshair lines once the QGraphicsScene is available.
57        Adds two semi-transparent yellow lines to the scene (horizontal + vertical).
58        """
59        if self.scene():
60            # Yellow, semi-transparent pen for crosshair
61            pen = QPen(QColor(255, 255, 0, 180), 1)
62
63            # Create horizontal and vertical crosshair lines in the scene
64            self.crosshair_h = self.scene().addLine(0, 0, 0, 0, pen)
65            self.crosshair_v = self.scene().addLine(0, 0, 0, 0, pen)
66
67            # Initially invisible
68            self.crosshair_h.setVisible(False)
69            self.crosshair_v.setVisible(False)

Initialize the crosshair lines once the QGraphicsScene is available. Adds two semi-transparent yellow lines to the scene (horizontal + vertical).

def mouseMoveEvent(self, event: PyQt6.QtGui.QMouseEvent):
74    def mouseMoveEvent(self, event: QMouseEvent):
75        """
76        Handle mouse movement:
77          - Maps cursor position to scene coordinates
78          - Checks bounds
79          - Updates crosshair lines
80          - Emits coordinate_changed signal
81        """
82        if self.scene() and self.parent_viewer and self.parent_viewer.img_data is not None:
83            # Map mouse position from view to scene coordinates
84            pos = self.mapToScene(event.pos())
85            x, y = int(pos.x()), int(pos.y())
86
87            # Get scene dimensions for bounds checking
88            scene_rect = self.scene().sceneRect()
89
90            # Only process if cursor is inside the image area
91            if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
92                # Move crosshair to current position
93                self.update_crosshairs(x, y)
94
95                # Notify parent about the new coordinates
96                self.coordinate_changed.emit(self.view_idx, x, y)
97
98        # Pass event to base class for default behavior
99        super().mouseMoveEvent(event)

Handle mouse movement:

  • Maps cursor position to scene coordinates
  • Checks bounds
  • Updates crosshair lines
  • Emits coordinate_changed signal
def wheelEvent(self, event: PyQt6.QtGui.QWheelEvent):
105    def wheelEvent(self, event: QWheelEvent):
106        delta = int(event.angleDelta().y()/120)
107        self.parent_viewer.handle_scroll(self.view_idx, delta)
108        event.accept()

wheelEvent(self, event: QWheelEvent|None)

def mousePressEvent(self, event: PyQt6.QtGui.QMouseEvent):
113    def mousePressEvent(self, event: QMouseEvent):
114        """
115        Handle mouse clicks:
116          - On left-click, compute image coordinates
117          - Notify parent viewer for cross-view synchronization or slice update
118        """
119        if (event.button() == Qt.MouseButton.LeftButton and
120                self.scene() and self.parent_viewer and
121                self.parent_viewer.img_data is not None):
122
123            # Convert click position to scene coordinates
124            pos = self.mapToScene(event.pos())
125            x, y = int(pos.x()), int(pos.y())
126
127            # Verify position is inside the scene
128            scene_rect = self.scene().sceneRect()
129            if 0 <= x < scene_rect.width() and 0 <= y < scene_rect.height():
130                # Delegate handling to parent viewer (e.g., update other views)
131                self.parent_viewer.handle_click_coordinates(self.view_idx, x, y)
132
133        super().mousePressEvent(event)

Handle mouse clicks:

  • On left-click, compute image coordinates
  • Notify parent viewer for cross-view synchronization or slice update
def update_crosshairs(self, x, y):
138    def update_crosshairs(self, x, y):
139        """
140        Move crosshair lines to a new position (x, y).
141        Makes them visible if hidden.
142        """
143        if self.crosshair_h and self.crosshair_v and self.scene():
144            scene_rect = self.scene().sceneRect()
145
146            # Horizontal line across the scene
147            self.crosshair_h.setLine(0, y, scene_rect.width(), y)
148
149            # Vertical line across the scene
150            self.crosshair_v.setLine(x, 0, x, scene_rect.height())
151
152            # If not visible yet, make them appear
153            if not self.crosshair_visible:
154                self.crosshair_h.setVisible(True)
155                self.crosshair_v.setVisible(True)
156                self.crosshair_visible = True

Move crosshair lines to a new position (x, y). Makes them visible if hidden.

def set_crosshair_position(self, x, y):
161    def set_crosshair_position(self, x, y):
162        """
163        Allows external components (e.g. linked views) to set the crosshair.
164        Ensures it's within bounds before updating.
165        """
166        if self.crosshair_h and self.crosshair_v and self.scene():
167            scene_rect = self.scene().sceneRect()
168            if (0 <= x < scene_rect.width() and 0 <= y < scene_rect.height()):
169                self.crosshair_h.setLine(0, y, scene_rect.width(), y)
170                self.crosshair_v.setLine(x, 0, x, scene_rect.height())
171                self.crosshair_h.setVisible(True)
172                self.crosshair_v.setVisible(True)
173                self.crosshair_visible = True

Allows external components (e.g. linked views) to set the crosshair. Ensures it's within bounds before updating.

def leaveEvent(self, event):
178    def leaveEvent(self, event):
179        """
180        Hide or update crosshairs when the mouse leaves the view area.
181        Currently, it calls the parent viewer’s update function to
182        synchronize crosshairs across multiple views.
183        """
184        if self.crosshair_h and self.crosshair_v:
185            # Previous behavior (hidden when leaving):
186            # self.crosshair_h.setVisible(False)
187            # self.crosshair_v.setVisible(False)
188            # self.crosshair_visible = False
189
190            # New behavior: ask parent to refresh all cross-view lines
191            self.parent_viewer.update_cross_view_lines()
192
193        super().leaveEvent(event)

Hide or update crosshairs when the mouse leaves the view area. Currently, it calls the parent viewer’s update function to synchronize crosshairs across multiple views.