149 lines
6.3 KiB
Python
149 lines
6.3 KiB
Python
|
|
"""
|
|||
|
|
Виджет для отображения и манипуляции солнечными изображениями
|
|||
|
|
Поддерживает: зум, панорамирование, композитинг слоев
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
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)
|