278 lines
10 KiB
Python
278 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
# gui/components/photo_view_widget.py
|
||
"""Виджет для отображения фотографий видов"""
|
||
|
||
from pathlib import Path
|
||
|
||
from PySide6.QtWidgets import QLabel, QSizePolicy
|
||
from PySide6.QtCore import Qt, Signal
|
||
from PySide6.QtGui import QPixmap
|
||
|
||
from gui.containers import VContainer, SContainer
|
||
from gui.styles import APP_STYLES
|
||
|
||
|
||
class PhotoViewWidget(SContainer):
|
||
"""Виджет для отображения фотографий видов"""
|
||
|
||
error_occurred = Signal(str)
|
||
|
||
def __init__(self, content_fit: bool = True):
|
||
super().__init__(width_percent=None, height_percent=None)
|
||
self._content_fit = content_fit
|
||
self._current_zoom = 1.0
|
||
self._current_view = None
|
||
self._images = {} # {имя_вида: QPixmap}
|
||
self._base_pixmap = None
|
||
self.setup_ui()
|
||
|
||
def setup_ui(self):
|
||
main_container = VContainer(
|
||
content_fit=self._content_fit,
|
||
parent=self,
|
||
)
|
||
|
||
self._image_label = QLabel()
|
||
self._image_label.setAlignment(Qt.AlignCenter)
|
||
self._image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||
self._image_label.setMinimumSize(1, 1)
|
||
|
||
# Важно: scaledContents=False, иначе некорректно комбинируется с зумом/KeepAspectRatio
|
||
self._image_label.setScaledContents(False)
|
||
|
||
# Устанавливаем фон как у главного окна
|
||
self.setStyleSheet(APP_STYLES.get("VIEW_WIDGET_BACKGROUND", ""))
|
||
self._image_label.setStyleSheet(APP_STYLES.get("VIEW_IMAGE_BACKGROUND", ""))
|
||
|
||
main_container.add_widget_with_stretch(self._image_label, 1)
|
||
def load_images(self, images_dict):
|
||
"""Загрузка изображений"""
|
||
self.clear_images()
|
||
|
||
if not images_dict:
|
||
self.show_error("Изображения не указаны")
|
||
return
|
||
|
||
loaded_count = 0
|
||
for view_name, image_path in images_dict.items():
|
||
p = Path(image_path)
|
||
if not p.exists():
|
||
continue
|
||
|
||
pixmap = QPixmap(str(p))
|
||
if pixmap.isNull():
|
||
continue
|
||
|
||
self._images[view_name] = pixmap
|
||
loaded_count += 1
|
||
|
||
if loaded_count == 0:
|
||
self.show_error("Не удалось загрузить ни одного изображения")
|
||
return
|
||
|
||
first_view = list(self._images.keys())[0]
|
||
self.show_view(first_view)
|
||
|
||
def clear_images(self):
|
||
"""Очистка изображений"""
|
||
self._images.clear()
|
||
self._current_view = None
|
||
self._base_pixmap = None
|
||
self._image_label.clear()
|
||
self._current_zoom = 1.0
|
||
|
||
def show_view(self, view_name):
|
||
"""Показать изображение вида"""
|
||
if view_name not in self._images:
|
||
return
|
||
|
||
self._current_view = view_name
|
||
self._base_pixmap = self._images[view_name]
|
||
self.update_display()
|
||
|
||
def switch_view_cycle(self, preferred_order):
|
||
"""Циклическое переключение вида с предпочтительным порядком"""
|
||
if not self._images or not self._current_view:
|
||
return
|
||
|
||
available_views = [v for v in preferred_order if v in self._images]
|
||
if not available_views:
|
||
return
|
||
|
||
if self._current_view in available_views:
|
||
current_index = available_views.index(self._current_view)
|
||
next_index = (current_index + 1) % len(available_views)
|
||
else:
|
||
next_index = 0
|
||
|
||
self.show_view(available_views[next_index])
|
||
|
||
def switch_to_isometric_or_next(self):
|
||
"""Переключение на изометрический вид или следующий в цикле"""
|
||
if not self._images:
|
||
return
|
||
|
||
if 'isometric' in self._images and self._current_view != 'isometric':
|
||
self.show_view('isometric')
|
||
return
|
||
|
||
available_views = list(self._images.keys())
|
||
if not available_views:
|
||
return
|
||
|
||
if self._current_view in available_views:
|
||
i = available_views.index(self._current_view)
|
||
self.show_view(available_views[(i + 1) % len(available_views)])
|
||
else:
|
||
self.show_view(available_views[0])
|
||
|
||
def update_display(self):
|
||
"""Обновление отображения изображения с учетом зума"""
|
||
if self._base_pixmap is None or self._base_pixmap.isNull():
|
||
return
|
||
|
||
# Базовый размер — размер label, дальше умножаем на zoom
|
||
target = self._image_label.size()
|
||
if target.width() <= 1 or target.height() <= 1:
|
||
return
|
||
|
||
w = max(1, int(target.width() * self._current_zoom))
|
||
h = max(1, int(target.height() * self._current_zoom))
|
||
|
||
scaled = self._base_pixmap.scaled(w, h, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||
self._image_label.setPixmap(scaled)
|
||
|
||
def zoom_in(self):
|
||
self._current_zoom = min(self._current_zoom * 1.2, 5.0)
|
||
self.update_display()
|
||
|
||
def zoom_out(self):
|
||
self._current_zoom = max(self._current_zoom / 1.2, 0.2)
|
||
self.update_display()
|
||
|
||
def reset_zoom(self):
|
||
self._current_zoom = 1.0
|
||
self.update_display()
|
||
|
||
def show_error(self, message):
|
||
self.clear_images()
|
||
self.error_occurred.emit(message)
|
||
|
||
def wheelEvent(self, event):
|
||
if self._images:
|
||
delta = event.angleDelta().y()
|
||
if delta > 0:
|
||
self.zoom_in()
|
||
else:
|
||
self.zoom_out()
|
||
event.accept()
|
||
|
||
def resizeEvent(self, event):
|
||
super().resizeEvent(event)
|
||
self.update_display()
|
||
|
||
def set_enabled(self, enabled: bool) -> None:
|
||
self.setEnabled(enabled)
|
||
|
||
def set_min_width(self, width: int) -> None:
|
||
self.setMinimumWidth(width)
|
||
|
||
def set_min_height(self, height: int) -> None:
|
||
self.setMinimumHeight(height)
|
||
|
||
def set_max_width(self, width: int) -> None:
|
||
self.setMaximumWidth(width)
|
||
|
||
def set_max_height(self, height: int) -> None:
|
||
self.setMaximumHeight(height)
|
||
|
||
def set_fixed_size(self, width: int, height: int) -> None:
|
||
self.setMinimumSize(width, height)
|
||
self.setMaximumSize(width, height)
|
||
|
||
def set_tooltip(self, text: str) -> None:
|
||
self.setToolTip(text)
|
||
|
||
def set_size_policy(self, horizontal, vertical) -> None:
|
||
self.setSizePolicy(horizontal, vertical)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Module workflow notes
|
||
# ---------------------------------------------------------------------------
|
||
#
|
||
# 1) Назначение модуля:
|
||
# Виджет для отображения фотографий видов детали с поддержкой
|
||
# зума (колесо мыши), циклического переключения видов и
|
||
# масштабирования с сохранением пропорций (KeepAspectRatio).
|
||
#
|
||
# 2) Зависимости модуля:
|
||
# Импорты: Path (pathlib),
|
||
# QLabel, QSizePolicy (PySide6.QtWidgets),
|
||
# Qt, Signal (PySide6.QtCore),
|
||
# QPixmap (PySide6.QtGui),
|
||
# VContainer, SContainer (gui.containers),
|
||
# APP_STYLES (gui.styles)
|
||
# Хост-класс / базовый класс: SContainer
|
||
# Внешние библиотеки: PySide6 (обязательна)
|
||
#
|
||
# 3) Экспорт:
|
||
# Класс PhotoViewWidget — виджет фотогалереи.
|
||
# Сигнал: error_occurred(str).
|
||
# Методы: load_images(dict), clear_images(), show_view(str),
|
||
# switch_view_cycle(list), switch_to_isometric_or_next(),
|
||
# update_display(), zoom_in(), zoom_out(), reset_zoom(),
|
||
# show_error().
|
||
#
|
||
# 4) Состояние (поля):
|
||
# _content_fit: bool — режим подгонки
|
||
# _current_zoom: float — текущий множитель зума (1.0 = 100%)
|
||
# _current_view: str|None — имя текущего вида
|
||
# _images: dict — {имя_вида: QPixmap}
|
||
# _base_pixmap: QPixmap|None— исходный pixmap текущего вида
|
||
# _image_label: QLabel — виджет отображения изображения
|
||
#
|
||
# 5) Последовательность действий и вызовов:
|
||
# __init__(content_fit)
|
||
# -> super().__init__(...)
|
||
# -> setup_ui() — VContainer + QLabel с Expanding + stylesheet
|
||
# load_images(images_dict)
|
||
# -> clear_images() -> для каждого пути: Path.exists() -> QPixmap
|
||
# -> show_view(first_view)
|
||
# show_view(view_name)
|
||
# -> _base_pixmap = _images[view_name] -> update_display()
|
||
# update_display()
|
||
# -> размер label * _current_zoom -> scaled(KeepAspectRatio, SmoothTransformation)
|
||
# -> _image_label.setPixmap(scaled)
|
||
# wheelEvent(event)
|
||
# -> delta > 0: zoom_in() | delta < 0: zoom_out()
|
||
# resizeEvent(event)
|
||
# -> update_display() — адаптация к новому размеру
|
||
#
|
||
# 6) Побочные эффекты:
|
||
# - Загружает файлы изображений с диска.
|
||
# - Устанавливает stylesheet через APP_STYLES.
|
||
# - Испускает error_occurred при ошибках.
|
||
#
|
||
# 7) Границы ответственности:
|
||
# Модуль НЕ управляет источником изображений (каталогом).
|
||
# НЕ редактирует файлы. НЕ подключается к theme_bus.
|
||
#
|
||
# 8) Обработка ошибок:
|
||
# load_images() проверяет Path.exists() и QPixmap.isNull().
|
||
# show_error() очищает изображения и испускает error_occurred.
|
||
#
|
||
# 9) Инварианты и контракты:
|
||
# - _current_zoom ∈ [0.2, 5.0].
|
||
# - scaledContents = False — масштабирование управляется вручную.
|
||
# - Если _base_pixmap is None — update_display() не делает ничего.
|
||
#
|
||
# 10) Правило сопровождения:
|
||
# Для добавления поддержки drag-and-drop или аннотаций — расширять
|
||
# подкласс, не модифицировать этот файл. Зум-границы (0.2–5.0) —
|
||
# константы в zoom_in/zoom_out.
|