feat(gui): add shared zoomable image preview
This commit is contained in:
@@ -0,0 +1,239 @@
|
|||||||
|
"""Reusable full-image preview dialog for desktop image workflows."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PySide6.QtCore import QSize, Qt, QTimer
|
||||||
|
from PySide6.QtGui import QImageReader, QPixmap
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QDialog,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QPushButton,
|
||||||
|
QScrollArea,
|
||||||
|
QToolButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImagePreviewDialog(QDialog):
|
||||||
|
"""Preview a local image with bounded zoom and fit-to-window behavior."""
|
||||||
|
|
||||||
|
MIN_ZOOM = 0.25
|
||||||
|
MAX_ZOOM = 4.0
|
||||||
|
ZOOM_STEP = 0.25
|
||||||
|
|
||||||
|
def __init__(self, path, title="图片预览", parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._base_title = str(title or "图片预览")
|
||||||
|
self._source = self._load_source(path)
|
||||||
|
self.fit_to_window = True
|
||||||
|
self.zoom_factor = 1.0
|
||||||
|
self._effective_zoom = 1.0
|
||||||
|
self._display_size = QSize()
|
||||||
|
self._resize_timer = QTimer(self)
|
||||||
|
self._resize_timer.setSingleShot(True)
|
||||||
|
self._resize_timer.setInterval(50)
|
||||||
|
self._resize_timer.timeout.connect(self._render)
|
||||||
|
|
||||||
|
self.setWindowTitle(self._window_title())
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.scroll = QScrollArea()
|
||||||
|
self.scroll.setObjectName("imagePreviewScrollArea")
|
||||||
|
self.scroll.setWidgetResizable(False)
|
||||||
|
self.scroll.setAlignment(Qt.AlignCenter)
|
||||||
|
self.image_label = QLabel()
|
||||||
|
self.image_label.setObjectName("imagePreviewLabel")
|
||||||
|
self.image_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.scroll.setWidget(self.image_label)
|
||||||
|
layout.addWidget(self.scroll, 1)
|
||||||
|
|
||||||
|
controls = QHBoxLayout()
|
||||||
|
self.zoom_out_button = QToolButton()
|
||||||
|
self.zoom_out_button.setObjectName("imagePreviewZoomOutButton")
|
||||||
|
self.zoom_out_button.setText("-")
|
||||||
|
self.zoom_out_button.setToolTip("缩小图片")
|
||||||
|
self.zoom_out_button.setAccessibleName("缩小图片")
|
||||||
|
self.zoom_out_button.setMinimumSize(32, 32)
|
||||||
|
self.zoom_out_button.clicked.connect(self.zoom_out)
|
||||||
|
controls.addWidget(self.zoom_out_button)
|
||||||
|
|
||||||
|
self.zoom_label = QLabel("适应窗口")
|
||||||
|
self.zoom_label.setObjectName("imagePreviewZoomLabel")
|
||||||
|
self.zoom_label.setAlignment(Qt.AlignCenter)
|
||||||
|
self.zoom_label.setMinimumWidth(72)
|
||||||
|
controls.addWidget(self.zoom_label)
|
||||||
|
|
||||||
|
self.zoom_in_button = QToolButton()
|
||||||
|
self.zoom_in_button.setObjectName("imagePreviewZoomInButton")
|
||||||
|
self.zoom_in_button.setText("+")
|
||||||
|
self.zoom_in_button.setToolTip("放大图片")
|
||||||
|
self.zoom_in_button.setAccessibleName("放大图片")
|
||||||
|
self.zoom_in_button.setMinimumSize(32, 32)
|
||||||
|
self.zoom_in_button.clicked.connect(self.zoom_in)
|
||||||
|
controls.addWidget(self.zoom_in_button)
|
||||||
|
|
||||||
|
self.fit_button = QPushButton("适应窗口")
|
||||||
|
self.fit_button.setObjectName("imagePreviewFitButton")
|
||||||
|
self.fit_button.setCheckable(True)
|
||||||
|
self.fit_button.setChecked(True)
|
||||||
|
self.fit_button.setToolTip("完整显示图片且不放大超过原始尺寸")
|
||||||
|
self.fit_button.clicked.connect(self.show_fit)
|
||||||
|
controls.addWidget(self.fit_button)
|
||||||
|
|
||||||
|
self.actual_size_button = QPushButton("100%")
|
||||||
|
self.actual_size_button.setObjectName("imagePreviewActualSizeButton")
|
||||||
|
self.actual_size_button.setToolTip("按图片原始像素查看")
|
||||||
|
self.actual_size_button.clicked.connect(self.show_actual_size)
|
||||||
|
controls.addWidget(self.actual_size_button)
|
||||||
|
|
||||||
|
controls.addStretch(1)
|
||||||
|
close_button = QPushButton("关闭")
|
||||||
|
close_button.clicked.connect(self.reject)
|
||||||
|
controls.addWidget(close_button)
|
||||||
|
layout.addLayout(controls)
|
||||||
|
|
||||||
|
self._fit_to_screen()
|
||||||
|
self._render()
|
||||||
|
QTimer.singleShot(0, self._render)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_source(path):
|
||||||
|
normalized = str(path or "")
|
||||||
|
if not normalized or not os.path.isfile(normalized):
|
||||||
|
return QPixmap()
|
||||||
|
reader = QImageReader(normalized)
|
||||||
|
reader.setAutoTransform(True)
|
||||||
|
image = reader.read()
|
||||||
|
return QPixmap.fromImage(image) if not image.isNull() else QPixmap()
|
||||||
|
|
||||||
|
def _window_title(self):
|
||||||
|
if self._source.isNull():
|
||||||
|
return self._base_title
|
||||||
|
return "%s · %dx%d" % (
|
||||||
|
self._base_title,
|
||||||
|
self._source.width(),
|
||||||
|
self._source.height(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
super().resizeEvent(event)
|
||||||
|
if self.fit_to_window:
|
||||||
|
self._resize_timer.start()
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
super().showEvent(event)
|
||||||
|
QTimer.singleShot(0, self._render)
|
||||||
|
|
||||||
|
def show_fit(self, checked=False):
|
||||||
|
self.fit_to_window = True
|
||||||
|
self._display_size = QSize()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def show_actual_size(self, checked=False):
|
||||||
|
self.fit_to_window = False
|
||||||
|
self.zoom_factor = 1.0
|
||||||
|
self._display_size = QSize()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def zoom_in(self, checked=False):
|
||||||
|
self._step_zoom(1)
|
||||||
|
|
||||||
|
def zoom_out(self, checked=False):
|
||||||
|
self._step_zoom(-1)
|
||||||
|
|
||||||
|
def _step_zoom(self, direction):
|
||||||
|
if self._source.isNull():
|
||||||
|
return
|
||||||
|
current = self._effective_zoom if self.fit_to_window else self.zoom_factor
|
||||||
|
if direction > 0:
|
||||||
|
stepped = (math.floor(current / self.ZOOM_STEP + 1e-9) + 1) * self.ZOOM_STEP
|
||||||
|
else:
|
||||||
|
stepped = (math.ceil(current / self.ZOOM_STEP - 1e-9) - 1) * self.ZOOM_STEP
|
||||||
|
self.fit_to_window = False
|
||||||
|
self.zoom_factor = max(self.MIN_ZOOM, min(self.MAX_ZOOM, stepped))
|
||||||
|
self._display_size = QSize()
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _fit_scale(self):
|
||||||
|
viewport = self.scroll.viewport().size()
|
||||||
|
if self._source.isNull() or viewport.width() <= 0 or viewport.height() <= 0:
|
||||||
|
return 1.0
|
||||||
|
return min(
|
||||||
|
1.0,
|
||||||
|
viewport.width() / self._source.width(),
|
||||||
|
viewport.height() / self._source.height(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _render(self):
|
||||||
|
viewport = self.scroll.viewport().size()
|
||||||
|
viewport_width = max(1, viewport.width())
|
||||||
|
viewport_height = max(1, viewport.height())
|
||||||
|
if self._source.isNull():
|
||||||
|
self.image_label.clear()
|
||||||
|
self.image_label.setText("图片文件不存在或无法读取")
|
||||||
|
self.image_label.resize(max(320, viewport_width), max(240, viewport_height))
|
||||||
|
self.zoom_label.setText("无法预览")
|
||||||
|
self._set_controls_enabled(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.fit_to_window:
|
||||||
|
scale = max(1e-6, min(1.0, self._fit_scale()))
|
||||||
|
else:
|
||||||
|
scale = max(self.MIN_ZOOM, min(self.MAX_ZOOM, self.zoom_factor))
|
||||||
|
self._effective_zoom = scale
|
||||||
|
display_size = QSize(
|
||||||
|
max(1, int(round(self._source.width() * scale))),
|
||||||
|
max(1, int(round(self._source.height() * scale))),
|
||||||
|
)
|
||||||
|
if display_size == self._source.size():
|
||||||
|
pixmap = self._source
|
||||||
|
else:
|
||||||
|
pixmap = self._source.scaled(
|
||||||
|
display_size,
|
||||||
|
Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation,
|
||||||
|
)
|
||||||
|
if display_size != self._display_size or self.image_label.pixmap() is None:
|
||||||
|
self.image_label.setPixmap(pixmap)
|
||||||
|
self.image_label.resize(pixmap.size())
|
||||||
|
self._display_size = QSize(pixmap.size())
|
||||||
|
self.zoom_label.setText(
|
||||||
|
"适应窗口" if self.fit_to_window else "%d%%" % int(round(scale * 100))
|
||||||
|
)
|
||||||
|
self.fit_button.setChecked(self.fit_to_window)
|
||||||
|
self._set_controls_enabled(True)
|
||||||
|
|
||||||
|
def _set_controls_enabled(self, available):
|
||||||
|
self.fit_button.setEnabled(available)
|
||||||
|
self.actual_size_button.setEnabled(available)
|
||||||
|
self.zoom_out_button.setEnabled(
|
||||||
|
available and self._effective_zoom > self.MIN_ZOOM + 1e-9
|
||||||
|
)
|
||||||
|
self.zoom_in_button.setEnabled(
|
||||||
|
available and self._effective_zoom < self.MAX_ZOOM - 1e-9
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fit_to_screen(self):
|
||||||
|
screen = self.parentWidget().screen() if self.parentWidget() is not None else None
|
||||||
|
screen = screen or QApplication.primaryScreen()
|
||||||
|
if screen is None:
|
||||||
|
self.resize(720, 520)
|
||||||
|
return
|
||||||
|
available = screen.availableGeometry()
|
||||||
|
max_width = max(1, int(available.width() * 0.9))
|
||||||
|
max_height = max(1, int(available.height() * 0.9))
|
||||||
|
min_width = min(560, max_width)
|
||||||
|
min_height = min(420, max_height)
|
||||||
|
source_width = self._source.width() if not self._source.isNull() else 640
|
||||||
|
source_height = self._source.height() if not self._source.isNull() else 480
|
||||||
|
self.resize(
|
||||||
|
min(max_width, max(min_width, source_width + 48)),
|
||||||
|
min(max_height, max(min_height, source_height + 104)),
|
||||||
|
)
|
||||||
|
frame = self.frameGeometry()
|
||||||
|
frame.moveCenter(available.center())
|
||||||
|
self.move(frame.topLeft())
|
||||||
@@ -9,6 +9,7 @@ from PySide6.QtWidgets import QListView, QListWidget, QListWidgetItem, QSizePoli
|
|||||||
|
|
||||||
from ... import accounts, appconfig, cmhub_models, db, diagnostics, image_studio, image_studio_export, image_studio_images, prompts
|
from ... import accounts, appconfig, cmhub_models, db, diagnostics, image_studio, image_studio_export, image_studio_images, prompts
|
||||||
from .. import file_manager
|
from .. import file_manager
|
||||||
|
from ..image_preview import ImagePreviewDialog
|
||||||
from ..widgets import *
|
from ..widgets import *
|
||||||
from ..workers import (
|
from ..workers import (
|
||||||
ImageStudioDownloadOriginalWorker as _RealImageStudioDownloadOriginalWorker,
|
ImageStudioDownloadOriginalWorker as _RealImageStudioDownloadOriginalWorker,
|
||||||
@@ -222,38 +223,13 @@ class ImageStudioSelectionList(QListWidget):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ImageStudioPreviewDialog(QDialog):
|
class ImageStudioPreviewDialog(ImagePreviewDialog):
|
||||||
"""Simple large image preview used by original and pool tables."""
|
"""Compatibility wrapper for the shared full-image preview."""
|
||||||
|
|
||||||
def __init__(self, asset, parent=None):
|
def __init__(self, asset, parent=None):
|
||||||
super().__init__(parent)
|
|
||||||
self.asset = asset
|
self.asset = asset
|
||||||
self.setWindowTitle(self._title_for_asset(asset))
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
scroll = QScrollArea()
|
|
||||||
scroll.setWidgetResizable(False)
|
|
||||||
image_label = QLabel()
|
|
||||||
image_label.setAlignment(Qt.AlignCenter)
|
|
||||||
path = str(getattr(asset, "local_path", "") or "")
|
path = str(getattr(asset, "local_path", "") or "")
|
||||||
image = QImage(path) if path and os.path.isfile(path) else QImage()
|
super().__init__(path, self._title_for_asset(asset), parent)
|
||||||
if image.isNull():
|
|
||||||
image_label.setText("图片尚未下载或读取失败")
|
|
||||||
image_label.setMinimumSize(420, 260)
|
|
||||||
else:
|
|
||||||
image_label.setPixmap(QPixmap.fromImage(image))
|
|
||||||
image_label.resize(image.size())
|
|
||||||
self.setWindowTitle(
|
|
||||||
f"{self._title_for_asset(asset)} · {image.width()}x{image.height()}"
|
|
||||||
)
|
|
||||||
scroll.setWidget(image_label)
|
|
||||||
layout.addWidget(scroll, 1)
|
|
||||||
buttons = QHBoxLayout()
|
|
||||||
buttons.addStretch(1)
|
|
||||||
close_button = QPushButton("关闭")
|
|
||||||
close_button.clicked.connect(self.reject)
|
|
||||||
buttons.addWidget(close_button)
|
|
||||||
layout.addLayout(buttons)
|
|
||||||
self.resize(720, 520)
|
|
||||||
|
|
||||||
def _title_for_asset(self, asset):
|
def _title_for_asset(self, asset):
|
||||||
badge = _asset_badge(getattr(asset, "kind", ""))
|
badge = _asset_badge(getattr(asset, "kind", ""))
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from PySide6.QtWidgets import (
|
|||||||
QApplication,
|
QApplication,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QDialog,
|
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
QFrame,
|
QFrame,
|
||||||
QGridLayout,
|
QGridLayout,
|
||||||
@@ -43,6 +42,7 @@ from PySide6.QtWidgets import (
|
|||||||
|
|
||||||
from ... import accounts, appconfig, diagnostics, image_studio, image_studio_images, product_suite
|
from ... import accounts, appconfig, diagnostics, image_studio, image_studio_images, product_suite
|
||||||
from .. import file_manager
|
from .. import file_manager
|
||||||
|
from ..image_preview import ImagePreviewDialog
|
||||||
from ..widgets import COLOR_DANGER, _emit_status, run_worker
|
from ..widgets import COLOR_DANGER, _emit_status, run_worker
|
||||||
from ..workers import (
|
from ..workers import (
|
||||||
ImageStudioDownloadOriginalWorker,
|
ImageStudioDownloadOriginalWorker,
|
||||||
@@ -109,42 +109,8 @@ def _user_error(error):
|
|||||||
return text if len(text) <= 90 else text[:87] + "..."
|
return text if len(text) <= 90 else text[:87] + "..."
|
||||||
|
|
||||||
|
|
||||||
class ProductSuitePreviewDialog(QDialog):
|
class ProductSuitePreviewDialog(ImagePreviewDialog):
|
||||||
"""Responsive preview for product originals and generated assets."""
|
"""Current product-suite wrapper for the shared full-image preview."""
|
||||||
|
|
||||||
def __init__(self, path, title="图片预览", parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._source = QPixmap(str(path or ""))
|
|
||||||
self.setWindowTitle(str(title or "图片预览"))
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
self.image_label = QLabel()
|
|
||||||
self.image_label.setAlignment(Qt.AlignCenter)
|
|
||||||
self.image_label.setMinimumSize(320, 240)
|
|
||||||
layout.addWidget(self.image_label, 1)
|
|
||||||
close_button = QPushButton("关闭")
|
|
||||||
close_button.clicked.connect(self.accept)
|
|
||||||
button_row = QHBoxLayout()
|
|
||||||
button_row.addStretch(1)
|
|
||||||
button_row.addWidget(close_button)
|
|
||||||
layout.addLayout(button_row)
|
|
||||||
self.resize(820, 620)
|
|
||||||
self._render()
|
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
|
||||||
super().resizeEvent(event)
|
|
||||||
self._render()
|
|
||||||
|
|
||||||
def _render(self):
|
|
||||||
if self._source.isNull():
|
|
||||||
self.image_label.setText("图片文件不存在或无法读取")
|
|
||||||
self.image_label.setPixmap(QPixmap())
|
|
||||||
return
|
|
||||||
target = self.image_label.size() - QSize(16, 16)
|
|
||||||
if target.width() <= 0 or target.height() <= 0:
|
|
||||||
return
|
|
||||||
self.image_label.setPixmap(
|
|
||||||
self._source.scaled(target, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProductOriginalDelegate(QStyledItemDelegate):
|
class ProductOriginalDelegate(QStyledItemDelegate):
|
||||||
|
|||||||
+6
-3
@@ -3,7 +3,7 @@ id: T-610
|
|||||||
title: AI工场原图预览默认适应窗口并支持缩放查看
|
title: AI工场原图预览默认适应窗口并支持缩放查看
|
||||||
phase: 7
|
phase: 7
|
||||||
deps: [T-609]
|
deps: [T-609]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-11
|
created: 2026-07-11
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ created: 2026-07-11
|
|||||||
|
|
||||||
### 1. 默认适应窗口显示完整图片
|
### 1. 默认适应窗口显示完整图片
|
||||||
|
|
||||||
修改 `app/gui/tabs/image_studio.py` 中的 `ImageStudioPreviewDialog`:
|
修改旧兼容 `app/gui/tabs/image_studio.py` 中的 `ImageStudioPreviewDialog`,并同步当前正式第六 Tab 使用的 `app/gui/tabs/product_suite.py::ProductSuitePreviewDialog`。两者复用同一个原生 PySide6 预览组件,避免只修复已隐藏的旧「AI工场」入口:
|
||||||
|
|
||||||
- 保留完整原始 `QPixmap`,不读取缩略图缓存、不重新下载、不修改本地图片。
|
- 保留完整原始 `QPixmap`,不读取缩略图缓存、不重新下载、不修改本地图片。
|
||||||
- 对可用图片,预览首次打开默认使用「适应窗口」模式:按 `QScrollArea.viewport()` 的实际可用尺寸等比缩放,保持原始比例,不裁切、不拉伸变形。
|
- 对可用图片,预览首次打开默认使用「适应窗口」模式:按 `QScrollArea.viewport()` 的实际可用尺寸等比缩放,保持原始比例,不裁切、不拉伸变形。
|
||||||
@@ -88,4 +88,7 @@ git diff --check
|
|||||||
|
|
||||||
## 执行记录
|
## 执行记录
|
||||||
|
|
||||||
- 待执行。
|
- 2026-07-16:新增 `app/gui/image_preview.py::ImagePreviewDialog`,保留完整本地图像,默认等比适应窗口且小图不放大;提供适应窗口、100%、缩小、放大和 25%~400% 固定倍率,固定倍率下窗口 resize 不重置,超大图片在适应模式下可低于 25% 以保证完整显示。
|
||||||
|
- 2026-07-16:旧兼容 `ImageStudioPreviewDialog` 与当前正式第六 Tab 的 `ProductSuitePreviewDialog` 改为复用同一预览组件;标题只显示中文上下文和原始分辨率,文件缺失时显示中文空状态并禁用缩放,不暴露本地路径。
|
||||||
|
- 2026-07-16:`tests/test_gui.py` 新增默认适应、100% 滚动查看、固定倍率 resize、缩放边界、小图不放大、旧入口复用和缺失文件回归测试。
|
||||||
|
- 验证:当前工作区定向 3 项 GUI 测试通过,ruff、compileall、`git diff --check` 通过;当前工作区全量测试仅受任务开始前未提交的默认封面提示词改名影响而失败 3 项。将本任务文件复制到基于 `HEAD` 的隔离 worktree 后,`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`(498 项)和 `git diff --check` 全部通过。
|
||||||
|
|||||||
@@ -54,8 +54,10 @@ from app.gui import (
|
|||||||
GenerateWorker,
|
GenerateWorker,
|
||||||
GenerateTab,
|
GenerateTab,
|
||||||
ForcedUpdateDialog,
|
ForcedUpdateDialog,
|
||||||
|
ImageStudioPreviewDialog,
|
||||||
ImageStudioTab,
|
ImageStudioTab,
|
||||||
MainWindow,
|
MainWindow,
|
||||||
|
ProductSuitePreviewDialog,
|
||||||
ProductSuiteTab,
|
ProductSuiteTab,
|
||||||
SettingsTab,
|
SettingsTab,
|
||||||
TAB_STYLE,
|
TAB_STYLE,
|
||||||
@@ -4515,6 +4517,121 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
|
|
||||||
self.assert_removed(temp_dir)
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_shared_image_preview_defaults_to_fit_and_keeps_fixed_zoom_on_resize(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
image_path = self.write_test_image(
|
||||||
|
os.path.join(temp_dir, "suite-large.jpg"),
|
||||||
|
width=1200,
|
||||||
|
height=800,
|
||||||
|
)
|
||||||
|
dialog = ProductSuitePreviewDialog(image_path, "商品原图预览")
|
||||||
|
self.addCleanup(dialog.close)
|
||||||
|
dialog.resize(600, 450)
|
||||||
|
dialog.show()
|
||||||
|
QApplication.processEvents()
|
||||||
|
dialog._resize_timer.stop()
|
||||||
|
dialog._render()
|
||||||
|
|
||||||
|
fit_pixmap = dialog.image_label.pixmap()
|
||||||
|
viewport = dialog.scroll.viewport().size()
|
||||||
|
self.assertTrue(dialog.fit_to_window)
|
||||||
|
self.assertEqual("适应窗口", dialog.zoom_label.text())
|
||||||
|
self.assertIn("1200x800", dialog.windowTitle())
|
||||||
|
self.assertLessEqual(fit_pixmap.width(), viewport.width())
|
||||||
|
self.assertLessEqual(fit_pixmap.height(), viewport.height())
|
||||||
|
self.assertAlmostEqual(1.5, fit_pixmap.width() / fit_pixmap.height(), places=1)
|
||||||
|
|
||||||
|
dialog.show_actual_size()
|
||||||
|
QApplication.processEvents()
|
||||||
|
self.assertFalse(dialog.fit_to_window)
|
||||||
|
self.assertEqual("100%", dialog.zoom_label.text())
|
||||||
|
self.assertEqual(QSize(1200, 800), dialog.image_label.pixmap().size())
|
||||||
|
self.assertEqual(QSize(1200, 800), dialog.image_label.size())
|
||||||
|
self.assertGreater(dialog.scroll.horizontalScrollBar().maximum(), 0)
|
||||||
|
self.assertGreater(dialog.scroll.verticalScrollBar().maximum(), 0)
|
||||||
|
|
||||||
|
dialog.resize(520, 360)
|
||||||
|
QApplication.processEvents()
|
||||||
|
dialog._resize_timer.stop()
|
||||||
|
dialog._render()
|
||||||
|
self.assertEqual(QSize(1200, 800), dialog.image_label.pixmap().size())
|
||||||
|
|
||||||
|
dialog.show_fit()
|
||||||
|
self.assertTrue(dialog.fit_to_window)
|
||||||
|
self.assertEqual("适应窗口", dialog.zoom_label.text())
|
||||||
|
self.assertLessEqual(
|
||||||
|
dialog.image_label.pixmap().width(),
|
||||||
|
dialog.scroll.viewport().width(),
|
||||||
|
)
|
||||||
|
self.assertLessEqual(
|
||||||
|
dialog.image_label.pixmap().height(),
|
||||||
|
dialog.scroll.viewport().height(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_shared_image_preview_bounds_zoom_and_does_not_enlarge_small_fit_image(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
image_path = self.write_test_image(
|
||||||
|
os.path.join(temp_dir, "suite-small.jpg"),
|
||||||
|
width=200,
|
||||||
|
height=100,
|
||||||
|
)
|
||||||
|
dialog = ProductSuitePreviewDialog(image_path, "图片预览")
|
||||||
|
self.addCleanup(dialog.close)
|
||||||
|
dialog.resize(700, 500)
|
||||||
|
dialog.show()
|
||||||
|
QApplication.processEvents()
|
||||||
|
dialog._resize_timer.stop()
|
||||||
|
dialog._render()
|
||||||
|
|
||||||
|
self.assertEqual(QSize(200, 100), dialog.image_label.pixmap().size())
|
||||||
|
for _index in range(20):
|
||||||
|
dialog.zoom_out()
|
||||||
|
self.assertEqual(0.25, dialog.zoom_factor)
|
||||||
|
self.assertEqual("25%", dialog.zoom_label.text())
|
||||||
|
self.assertFalse(dialog.zoom_out_button.isEnabled())
|
||||||
|
|
||||||
|
for _index in range(20):
|
||||||
|
dialog.zoom_in()
|
||||||
|
self.assertEqual(4.0, dialog.zoom_factor)
|
||||||
|
self.assertEqual("400%", dialog.zoom_label.text())
|
||||||
|
self.assertFalse(dialog.zoom_in_button.isEnabled())
|
||||||
|
self.assertEqual(QSize(800, 400), dialog.image_label.pixmap().size())
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_shared_image_preview_legacy_wrapper_and_missing_file_state(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
image_path = self.write_test_image(
|
||||||
|
os.path.join(temp_dir, "legacy.jpg"),
|
||||||
|
width=320,
|
||||||
|
height=240,
|
||||||
|
)
|
||||||
|
asset = SimpleNamespace(id=7, kind="original", local_path=image_path)
|
||||||
|
legacy = ImageStudioPreviewDialog(asset)
|
||||||
|
self.addCleanup(legacy.close)
|
||||||
|
legacy.show()
|
||||||
|
QApplication.processEvents()
|
||||||
|
legacy._resize_timer.stop()
|
||||||
|
legacy._render()
|
||||||
|
|
||||||
|
self.assertIn("AI工场图片预览", legacy.windowTitle())
|
||||||
|
self.assertIn("320x240", legacy.windowTitle())
|
||||||
|
self.assertEqual("适应窗口", legacy.zoom_label.text())
|
||||||
|
|
||||||
|
missing_path = os.path.join(temp_dir, "missing-private-name.jpg")
|
||||||
|
missing = ProductSuitePreviewDialog(missing_path, "商品原图预览")
|
||||||
|
self.addCleanup(missing.close)
|
||||||
|
missing._render()
|
||||||
|
self.assertEqual("图片文件不存在或无法读取", missing.image_label.text())
|
||||||
|
self.assertEqual("无法预览", missing.zoom_label.text())
|
||||||
|
self.assertFalse(missing.fit_button.isEnabled())
|
||||||
|
self.assertFalse(missing.actual_size_button.isEnabled())
|
||||||
|
self.assertNotIn(missing_path, missing.windowTitle())
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
def test_cover_gallery_previous_next_switch_visible_tasks_without_resizing(self):
|
def test_cover_gallery_previous_next_switch_visible_tasks_without_resizing(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
cfg, account, tasks, canonicals, _archives = self._cover_gallery_task_set(temp_dir, count=3)
|
cfg, account, tasks, canonicals, _archives = self._cover_gallery_task_set(temp_dir, count=3)
|
||||||
|
|||||||
Reference in New Issue
Block a user