Add Dispatch_V0.1.1
This commit is contained in:
277
Dispatch_V0.1.1/gui/components/photo_view_widget.py
Normal file
277
Dispatch_V0.1.1/gui/components/photo_view_widget.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# -*- 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.
|
||||
Reference in New Issue
Block a user