""" Виджет для отображения и манипуляции солнечными изображениями Поддерживает: зум, панорамирование, композитинг слоев """ from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem from PySide6.QtCore import Qt, QRectF, QPointF, Signal from PySide6.QtGui import QPixmap, QImage, QWheelEvent, QMouseEvent, QPainter import numpy as np class SolarCanvas(QGraphicsView): """ Кастомный Canvas для отображения солнечных изображений Поддерживает масштабирование, панорамирование и наложение слоев """ zoom_changed = Signal(float) def __init__(self, controller): super().__init__() self.controller = controller self.scene = QGraphicsScene(self) self.setScene(self.scene) # Настройки отображения - ИСПРАВЛЕНО: используем QPainter self.setRenderHint(QPainter.RenderHint.Antialiasing) self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) self.setDragMode(self.DragMode.ScrollHandDrag) self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse) self.setResizeAnchor(self.ViewportAnchor.AnchorUnderMouse) # Переменные для зума self.current_zoom = 1.0 self.min_zoom = 0.1 self.max_zoom = 10.0 self.zoom_factor = 1.1 # Словарь для хранения слоев (QGraphicsPixmapItem) self.layer_items = {} # Фон для лучшего контраста (черный для астрономических изображений) self.setBackgroundBrush(Qt.GlobalColor.black) # Дополнительные настройки для плавности self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) def set_image(self, layer_id: int, image_data: np.ndarray): """ Устанавливает изображение для слоя Args: layer_id: ID слоя image_data: numpy массив с изображением """ # Конвертируем numpy array в QImage height, width = image_data.shape[:2] if len(image_data.shape) == 2: # Черно-белое изображение bytes_per_line = width qimage = QImage(image_data.data, width, height, bytes_per_line, QImage.Format.Format_Grayscale8) else: # RGB изображение bytes_per_line = 3 * width qimage = QImage(image_data.data, width, height, bytes_per_line, QImage.Format.Format_RGB888) # Конвертируем в QPixmap pixmap = QPixmap.fromImage(qimage) # Создаем или обновляем графический элемент if layer_id in self.layer_items: self.layer_items[layer_id].setPixmap(pixmap) else: item = self.scene.addPixmap(pixmap) self.layer_items[layer_id] = item # Центрируем изображение при первом добавлении if len(self.layer_items) == 1: self.centerOn(item) self.fitInView(item, Qt.AspectRatioMode.KeepAspectRatio) def set_layer_visibility(self, layer_id: int, visible: bool): """Устанавливает видимость слоя""" if layer_id in self.layer_items: self.layer_items[layer_id].setVisible(visible) def set_layer_opacity(self, layer_id: int, opacity: float): """Устанавливает прозрачность слоя""" if layer_id in self.layer_items: self.layer_items[layer_id].setOpacity(opacity) def remove_layer(self, layer_id: int): """Удаляет слой""" if layer_id in self.layer_items: self.scene.removeItem(self.layer_items[layer_id]) del self.layer_items[layer_id] def clear_all_layers(self): """Очищает все слои""" for item in self.layer_items.values(): self.scene.removeItem(item) self.layer_items.clear() def wheelEvent(self, event: QWheelEvent): """Обработка колесика мыши для зума""" # Определяем направление зума zoom_in = event.angleDelta().y() > 0 # Вычисляем новый уровень зума if zoom_in: new_zoom = self.current_zoom * self.zoom_factor else: new_zoom = self.current_zoom / self.zoom_factor # Ограничиваем зум if self.min_zoom <= new_zoom <= self.max_zoom: self.current_zoom = new_zoom self.scale(self.zoom_factor if zoom_in else 1/self.zoom_factor, self.zoom_factor if zoom_in else 1/self.zoom_factor) self.zoom_changed.emit(self.current_zoom) def mousePressEvent(self, event: QMouseEvent): """Обработка нажатия мыши для панорамирования""" if event.button() == Qt.MouseButton.MiddleButton: self.setDragMode(self.DragMode.ScrollHandDrag) # Создаем фейковое событие с левой кнопкой fake_event = QMouseEvent( event.type(), event.pos(), Qt.MouseButton.LeftButton, Qt.MouseButton.LeftButton, event.modifiers() ) super().mousePressEvent(fake_event) else: super().mousePressEvent(event) def mouseReleaseEvent(self, event: QMouseEvent): """Обработка отпускания мыши""" if event.button() == Qt.MouseButton.MiddleButton: self.setDragMode(self.DragMode.NoDrag) super().mouseReleaseEvent(event) def reset_view(self): """Сбрасывает зум и позицию""" if self.layer_items: first_item = next(iter(self.layer_items.values())) self.fitInView(first_item, Qt.AspectRatioMode.KeepAspectRatio) self.current_zoom = 1.0 self.zoom_changed.emit(self.current_zoom)