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)
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
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)
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).
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
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)
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
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.
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.
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.