MetaCore-startup/MetaCore/ui/project_card.py
2025-10-11 09:27:51 +08:00

975 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
项目卡片组件
"""
import os
import sys
import subprocess
import platform
from pathlib import Path
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from MetaCore.data.project_manager import ProjectManager, Project
from MetaCore.ui.icon_manager import IconManager
from MetaCore.ui.project_settings_page import ProjectSettingsPage
class ImageDisplayWidget(QWidget):
"""
一个专门用于显示带圆角图片的控件。
它会自动将图片按比例缩放以覆盖整个区域,并进行圆角裁剪。
"""
def __init__(self, parent=None):
super().__init__(parent)
self.pixmap = QPixmap()
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def setPixmap(self, pixmap):
self.pixmap = pixmap
self.update() # 触发重绘
# 这是在 ImageDisplayWidget 类内部的方法
def paintEvent(self, event):
if self.pixmap.isNull():
return
# 导入 QSize 以便创建新的尺寸对象
from PyQt5.QtCore import QSize
import math
target_rect = self.rect()
radius = 12.0
# “过缩放”逻辑保持不变,我们仍然需要一张比控件大的图片
overscale_factor = 1.05
original_size = target_rect.size()
larger_width = math.ceil(original_size.width() * overscale_factor)
larger_height = math.ceil(original_size.height() * overscale_factor)
larger_target_size = QSize(larger_width, larger_height)
scaled_pixmap = self.pixmap.scaled(larger_target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
# --- 核心修正点在这里 ---
# 1. 计算绘制的起始坐标(x, y),以使 scaled_pixmap 的中心与控件中心对齐
# 由于 scaled_pixmap 比控件大,所以 x 和 y 通常是负数,这是正确的
draw_x = (target_rect.width() - scaled_pixmap.width()) / 2
draw_y = (target_rect.height() - scaled_pixmap.height()) / 2
# 2. 使用QPainter进行绘制
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 3. 创建并设置圆角裁剪路径 (逻辑不变)
path = QPainterPath()
path.addRoundedRect(QRectF(target_rect), radius, radius)
painter.setClipPath(path)
# 4. 使用最简单的 drawPixmap 版本,直接在计算好的坐标点绘制
# 这个版本不会进行任何额外的缩放,完美解决了偏移问题
painter.drawPixmap(int(draw_x), int(draw_y), scaled_pixmap)
# -----------------------
class ProjectCard(QWidget):
"""项目卡片组件"""
def __init__(self, project: Project, project_manager: ProjectManager, view_mode: str = "grid"):
super().__init__()
self.project = project
self.project_manager = project_manager
self.view_mode = view_mode
self.project_settings_page = ProjectSettingsPage()
# 设置卡片对象名称和属性
self.setObjectName("projectCard")
self.setProperty("status", project.status)
self.setAttribute(Qt.WA_StyledBackground) # 关键:启用样式背景继承
# 设置尺寸和策略
if view_mode == "grid":
self.setFixedSize(280, 240)
else:
self.setFixedHeight(80)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.init_ui()
self.connect_signals()
# 确保样式正确应用
self.update_style()
def update_style(self):
"""强制刷新样式"""
self.style().unpolish(self)
self.style().polish(self)
self.update()
def init_ui(self):
"""初始化UI"""
if self.view_mode == "grid":
self.create_grid_layout()
else:
self.create_list_layout()
def create_grid_layout(self):
"""创建网格布局"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
# 项目头部
self.create_project_header(layout)
# 项目图片
self.create_project_image(layout)
# 项目底部
self.create_project_footer(layout)
def create_list_layout(self):
"""创建列表布局"""
layout = QHBoxLayout(self)
layout.setContentsMargins(16, 12, 16, 12)
layout.setSpacing(16)
# 项目图标 - 使用优化的图标显示
self.create_list_project_icon(layout)
# 项目信息
info_layout = QVBoxLayout()
info_layout.setSpacing(4)
# 项目名称
title_layout = QHBoxLayout()
title_label = QLabel(self.project.title)
title_label.setObjectName("projectTitle")
title_layout.addWidget(title_label)
title_layout.addStretch()
info_layout.addLayout(title_layout)
# 项目日期
date_label = QLabel(self.project.date)
date_label.setObjectName("projectDate")
info_layout.addWidget(date_label)
layout.addLayout(info_layout)
layout.addStretch()
# 菜单按钮
self.create_menu_button(layout)
def create_project_header(self, layout):
"""创建项目头部"""
header_widget = QWidget()
header_widget.setObjectName("projectHeader")
header_layout = QHBoxLayout(header_widget)
header_layout.setContentsMargins(16, 16, 16, 8)
header_layout.setSpacing(8)
# 项目标题
title_label = QLabel(self.project.title)
title_label.setObjectName("projectTitle")
title_label.setWordWrap(True)
header_layout.addWidget(title_label, 1) # 添加拉伸因子
# 右侧操作区
actions_layout = QHBoxLayout()
actions_layout.setSpacing(4)
# 根据项目状态显示不同的按钮
if self.project.status == 'pending_delete':
self.menu_btn = QPushButton("")
self.menu_btn.setObjectName("deleteBtn")
self.menu_btn.setFixedSize(24, 24)
self.menu_btn.clicked.connect(self.confirm_delete_project)
self.menu_btn.setToolTip("确认删除项目")
else:
self.menu_btn = QPushButton("")
self.menu_btn.setObjectName("menuBtn")
self.menu_btn.setFixedSize(24, 24)
self.menu_btn.clicked.connect(self.show_context_menu)
self.menu_btn.setToolTip("项目操作菜单")
actions_layout.addWidget(self.menu_btn)
header_layout.addLayout(actions_layout)
layout.addWidget(header_widget)
def create_project_image(self, layout):
"""创建项目图片区域 - 最终版"""
import os
from PyQt5.QtWidgets import QStackedLayout # 导入 QStackedLayout
# image_container 是最外层的容器
image_container = QWidget()
image_container.setObjectName("projectImageContainer")
image_container.setContentsMargins(16, 0, 16, 0)
# image_widget 现在是堆叠布局的容器,它拥有灰色背景和圆角
image_widget = QWidget()
image_widget.setObjectName("projectImage")
# --- 使用 QStackedLayout 来切换两种状态 ---
stacked_layout = QStackedLayout(image_widget)
stacked_layout.setContentsMargins(0, 0, 0, 0)
# 状态一:显示图片 (使用我们自定义的控件)
image_display = ImageDisplayWidget()
# 注意:这里不需要给 image_display 设置 objectName 或样式,因为它只是一个“画布”
# 状态二:显示默认图标 (使用一个标准的 QLabel)
default_icon_label = QLabel("📁")
default_icon_label.setObjectName("projectIcon") # QSS 会应用到这里
default_icon_label.setAlignment(Qt.AlignCenter)
# 将两个状态控件添加到堆叠布局中
stacked_layout.addWidget(image_display) # 索引 0
stacked_layout.addWidget(default_icon_label) # 索引 1
# --- 根据条件切换显示 ---
if hasattr(self.project, 'image') and os.path.exists(self.project.image) and os.path.isfile(self.project.image):
# 如果图片存在...
pixmap = QPixmap(self.project.image)
image_display.setPixmap(pixmap)
stacked_layout.setCurrentIndex(0) # 显示图片控件
else:
# 如果图片不存在...
stacked_layout.setCurrentIndex(1) # 显示默认图标控件
# --- 最终布局 ---
# 我们只需要一个简单的 QVBoxLayout 来容纳 image_widget
container_layout = QVBoxLayout(image_container)
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.addWidget(image_widget)
layout.addWidget(image_container, 1)
def create_project_footer(self, layout):
"""创建项目底部"""
footer_widget = QWidget()
footer_widget.setObjectName("projectFooter")
footer_layout = QVBoxLayout(footer_widget)
footer_layout.setContentsMargins(16, 0, 16, 16) # 减少顶部边距
# 项目日期
date_label = QLabel(self.project.date)
date_label.setObjectName("projectDate")
date_label.setAlignment(Qt.AlignCenter)
footer_layout.addWidget(date_label)
layout.addWidget(footer_widget)
def create_menu_button(self, layout):
"""创建菜单按钮(用于列表视图)"""
self.menu_btn = QPushButton("")
self.menu_btn.setObjectName("menuBtn")
self.menu_btn.setFixedSize(32, 32)
self.menu_btn.clicked.connect(self.show_context_menu)
layout.addWidget(self.menu_btn)
def create_list_project_icon(self, layout):
"""创建列表视图中的项目图标"""
import os
from PyQt5.QtWidgets import QStackedLayout
# 图标容器
icon_container = QWidget()
icon_container.setObjectName("listProjectIconContainer")
icon_container.setFixedSize(48, 48) # 列表视图中使用较小的图标
# 图标控件 - 带圆角背景
icon_widget = QWidget()
icon_widget.setObjectName("listProjectIcon")
icon_widget.setProperty("status", self.project.status) # 设置状态属性
# 使用堆叠布局来切换图片和默认图标
stacked_layout = QStackedLayout(icon_widget)
stacked_layout.setContentsMargins(0, 0, 0, 0)
# 状态一:显示图片 (使用自定义的圆角图片控件)
image_display = ListImageDisplayWidget()
# 状态二:显示默认图标
default_icon_label = QLabel("📁")
default_icon_label.setObjectName("listProjectDefaultIcon")
default_icon_label.setAlignment(Qt.AlignCenter)
# 添加到堆叠布局
stacked_layout.addWidget(image_display) # 索引 0
stacked_layout.addWidget(default_icon_label) # 索引 1
# 根据条件切换显示
if hasattr(self.project, 'image') and self.project.image and os.path.exists(self.project.image) and os.path.isfile(self.project.image):
# 如果图片存在,显示图片
pixmap = QPixmap(self.project.image)
if not pixmap.isNull():
image_display.setPixmap(pixmap)
stacked_layout.setCurrentIndex(0)
else:
stacked_layout.setCurrentIndex(1)
else:
# 如果图片不存在,显示默认图标
stacked_layout.setCurrentIndex(1)
# 容器布局
container_layout = QVBoxLayout(icon_container)
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.addWidget(icon_widget)
layout.addWidget(icon_container)
def get_type_label(self):
"""获取项目类型标签"""
type_labels = {
'industrial': '工业',
'smart': '智能',
'vr': 'VR',
'game': '游戏',
'design': '设计'
}
return type_labels.get(self.project.type, '其他')
def show_context_menu(self):
"""显示右键菜单"""
menu = QMenu(self)
# 刷新预览图
if IconManager.icon_exists("Refresh"):
refresh_action = menu.addAction(IconManager.get_icon('Refresh'), "刷新预览图")
else:
refresh_action = menu.addAction("🔄 刷新预览图")
refresh_action.triggered.connect(self.refresh_preview_image)
menu.addSeparator()
# 在资源管理器显示
if IconManager.icon_exists('folder'):
show_in_explorer_action = menu.addAction(IconManager.get_icon('folder'), "在资源管理器显示")
else:
show_in_explorer_action = menu.addAction("📁 在资源管理器显示")
show_in_explorer_action.triggered.connect(self.show_in_explorer)
menu.addSeparator()
# 删除项目
if IconManager.icon_exists('delete'):
delete_action = menu.addAction(IconManager.get_icon('delete'), "移除项目")
else:
delete_action = menu.addAction("🗑️ 移除项目")
delete_action.triggered.connect(self.delete_project)
# 显示菜单
menu.exec_(self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft()))
def open_project(self):
"""打开项目"""
try:
# 优先使用项目目录路径,如果不存在则使用基础路径
project_path = self.project.project_dir if self.project.project_dir else self.project.path
# 使用项目管理器的验证方法进行全面检查
is_valid, error_message = self.project_manager.validate_project_open(project_path)
if not is_valid:
QMessageBox.warning(self, "无法打开项目", f"项目无法打开: {error_message}")
return
# 验证通过,显示成功信息
# QMessageBox.information(self, "打开项目", f"正在打开项目: {self.project.title}")
# 使用question对话框提供明确的确认选项
reply = QMessageBox.question(self, "打开项目",
f"确定要打开项目: {self.project.title}?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes)
if reply == QMessageBox.Yes:
# 用户确认打开项目,继续执行打开逻辑
# TODO: 在这里添加实际的项目打开逻辑
print(f"正在打开项目路径: {project_path},正在启动应用程序: {self.project.title}")
# 连接信号
self.project_manager.pycharm_started.connect(self.on_pycharm_started)
self.project_manager.project_method_called.connect(self.on_project_method_called)
# # 启动PyCharm并调用项目方法
# # 修正参数顺序:
# # 第一个参数要在PyCharm中打开的项目路径EG项目
# # 第二个参数:要传递给项目方法的目标项目路径(当前项目)
success = self.project_manager.run_project_command(
self.project_settings_page.get_default_open_location(),
# "/home/tiger/文档/EG", # project_path - 要在PyCharm中打开的项目
project_path # target_project_path - 要传递给项目方法的目标项目路径
)
if not success:
QMessageBox.warning(
self,
"启动失败",
"无法启动,请检查打开项目路径。"
)
# if success:
# QMessageBox.information(
# self,
# "启动PyCharm",
# f"正在启动PyCharm并准备调用项目方法...\n目标项目: {project_path}"
# )
# else:
# QMessageBox.warning(
# self,
# "启动失败",
# "无法启动PyCharm请检查PyCharm安装。"
# )
else:
# 用户取消操作,直接返回
return
# success = self.project_manager.launch_pycharm_and_open_project(
# "/home/tiger/文档/EG", # project_path - 要在PyCharm中打开的项目
# project_path # target_project_path - 要传递给项目方法的目标项目路径
# )
# if success:
# QMessageBox.information(
# self,
# "启动PyCharm",
# f"正在启动PyCharm并准备调用项目方法...\n目标项目: {project_path}"
# )
# else:
# QMessageBox.warning(
# self,
# "启动失败",
# "无法启动PyCharm请检查PyCharm安装。"
# )
# self.project_manager.open_app_if_not_running(f'{project_path}/project.json', "project.json")
# self.project_manager.open_project_in_pycharm(project_path)
# except Exception as e:
# QMessageBox.critical(self, "错误", f"打开项目时发生错误:\n{str(e)}")
except Exception as e:
QMessageBox.critical(self, "错误", f"启动PyCharm时发生错误:\n{str(e)}")
def on_pycharm_started(self):
"""PyCharm启动完成回调"""
print("PyCharm has started and is ready")
def on_project_method_called(self, target_project_path):
"""项目方法调用完成回调"""
QMessageBox.information(
self,
"操作完成",
f"已成功在PyCharm中打开项目: {target_project_path}"
)
def show_in_explorer(self):
"""在资源管理器中显示项目目录"""
try:
# 获取项目路径并规范化
project_path_str = self.project.project_dir if self.project.project_dir else self.project.path
if not project_path_str:
QMessageBox.warning(self, "路径不存在", "项目路径为空,请检查项目配置。")
return
# 使用pathlib处理路径
project_path = Path(project_path_str).resolve()
print(f"项目ID: {self.project.id}")
print(f"项目标题: {self.project.title}")
print(f"最终使用的路径: '{project_path}'")
print(f"路径是否存在: {project_path.exists()}")
print(f"是否为目录: {project_path.is_dir()}")
# 检查项目路径是否存在
if not project_path.exists():
QMessageBox.warning(self, "路径不存在",
f"项目路径不存在或无效:\n{project_path}\n\n请检查项目是否已被移动或删除。")
return
# 获取操作系统类型
system = platform.system().lower()
if system == "windows":
try:
# Windows使用os.startfile会自动使用系统默认的文件管理器
os.startfile(str(project_path))
except OSError as e:
QMessageBox.critical(self, "打开失败", f"无法打开资源管理器:\n{str(e)}")
elif system == "darwin": # macOS
if project_path.is_file():
# 如果是文件,选中该文件
subprocess.run(['open', '-R', str(project_path)], check=True)
else:
# 如果是目录,直接打开
subprocess.run(['open', str(project_path)], check=True)
else: # Linux和其他Unix系统
try:
# 首先尝试使用nautilusGNOME文件管理器
subprocess.run(['nautilus', str(project_path)], check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
try:
# 尝试使用dolphin (KDE)
subprocess.run(['dolphin', str(project_path)], check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
try:
# 尝试使用thunar (XFCE)
subprocess.run(['thunar', str(project_path)], check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
try:
# 最后尝试使用xdg-open
subprocess.run(['xdg-open', str(project_path)], check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
QMessageBox.warning(self, "无法打开文件管理器",
"系统中没有找到合适的文件管理器。\n"
f"请手动打开路径: {project_path}")
print(f"成功在资源管理器中打开: {project_path}")
except subprocess.CalledProcessError as e:
QMessageBox.critical(self, "打开失败",
f"无法打开资源管理器:\n{str(e)}")
except Exception as e:
QMessageBox.critical(self, "错误",
f"打开资源管理器时发生错误:\n{str(e)}")
def delete_project(self):
"""删除项目"""
reply = QMessageBox.question(self, "确认移除",
f"确定要移除项目 \"{self.project.title}\" 吗?\n此操作不可撤销。",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if reply == QMessageBox.Yes:
self.project_manager.remove_project(self.project.id)
def confirm_delete_project(self):
"""确认删除待删除状态的项目"""
reply = QMessageBox.question(self, "确认删除项目",
f"确定要永久删除项目 \"{self.project.title}\" 吗?\n"
f"此操作不可撤销。\n\n"
f"提示:如果项目目录已恢复,您可以点击项目卡片来恢复项目。",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if reply == QMessageBox.Yes:
self.project_manager.confirm_delete_project(self.project.id)
def update_display(self):
"""更新显示"""
# 更新项目状态属性
self.setProperty("status", self.project.status)
# 重新创建UI以反映更改
for i in reversed(range(self.layout().count())):
child = self.layout().itemAt(i).widget()
if child:
child.setParent(None)
self.init_ui()
# 强制刷新样式
self.style().unpolish(self)
self.style().polish(self)
self.update()
def apply_fallback_styles(self):
"""应用备用样式,确保卡片有正确的外观"""
# 直接设置卡片的内联样式
card_style = """
QWidget#projectCard {
background-color: #4a4a5a;
border: 1px solid #5a5a6a;
border-radius: 16px;
}
QWidget#projectCard:hover {
background-color: #5a5a6a;
border-color: #6a6a7a;
}
"""
self.setStyleSheet(card_style)
def connect_signals(self):
"""连接信号"""
pass
def mousePressEvent(self, event):
"""鼠标点击事件"""
if event.button() == Qt.LeftButton:
# 检查是否点击了菜单按钮
if not self.menu_btn.geometry().contains(event.pos()):
if self.project.status == 'pending_delete':
# 待删除状态的项目,尝试恢复
self.try_restore_project()
else:
# 正常状态的项目,打开项目
self.open_project()
super().mousePressEvent(event)
def try_restore_project(self):
"""尝试恢复待删除状态的项目"""
if self.project_manager.restore_project(self.project.id):
QMessageBox.information(self, "项目已恢复",
f"项目 \"{self.project.title}\" 已成功恢复!")
else:
QMessageBox.information(self, "项目目录不存在",
f"项目 \"{self.project.title}\" 的目录仍然不存在:\n{self.project.project_dir}\n\n"
f"提示:当您恢复项目目录后,系统会自动检测并恢复项目状态,无需手动操作。")
def enterEvent(self, event):
"""鼠标进入事件"""
self.setProperty("hover", True)
self.style().unpolish(self)
self.style().polish(self)
# 强制重绘以确保悬停效果立即生效
self.update()
super().enterEvent(event)
def leaveEvent(self, event):
"""鼠标离开事件"""
self.setProperty("hover", False)
self.style().unpolish(self)
self.style().polish(self)
# 强制重绘以确保悬停效果立即消失
self.update()
super().leaveEvent(event)
def show_success_tooltip(self, message):
"""显示成功提示工具提示"""
# 临时改变菜单按钮的工具提示来显示成功状态
original_tooltip = self.menu_btn.toolTip()
self.menu_btn.setToolTip(f"{message}")
# 使用定时器恢复原状态
QTimer.singleShot(3000, lambda: self.restore_button_state(original_tooltip))
def restore_button_state(self, original_tooltip):
"""恢复按钮原始状态"""
self.menu_btn.setToolTip(original_tooltip)
def refresh_preview_image(self):
"""刷新预览图"""
try:
# 获取项目路径
project_path = self.project.project_dir if self.project.project_dir else self.project.path
if not project_path or not Path(project_path).exists():
QMessageBox.warning(self, "路径不存在", "项目路径不存在,无法生成预览图。")
return
# 显示进度提示
original_tooltip = self.menu_btn.toolTip()
self.menu_btn.setEnabled(False)
self.menu_btn.setToolTip("⏳ 正在生成预览图...")
QApplication.processEvents()
# 生成新的预览图
preview_path = self.generate_project_preview(project_path)
if preview_path and Path(preview_path).exists():
# 清理旧的预览图
self.project_manager.cleanup_old_preview_images(self.project.id)
# 更新项目的图片路径
old_image = self.project.image
self.project.image = preview_path
self.project_manager.update_project(self.project)
# 刷新显示
self.refresh_image_display()
# 显示成功提示(不使用阻塞对话框)
self.show_success_tooltip("预览图已刷新!")
# 如果有旧图片且不同于新图片,尝试删除
if old_image and old_image != preview_path and Path(old_image).exists():
try:
# 检查是否是我们生成的预览图
if "ProjectPreviews" in old_image:
Path(old_image).unlink()
except Exception:
pass # 忽略删除失败
else:
QMessageBox.warning(self, "生成失败",
"无法生成预览图。\n\n可能原因:\n"
"• 项目目录中没有图片文件\n"
"• 图片文件格式不支持\n"
"• 权限不足")
except Exception as e:
QMessageBox.critical(self, "错误", f"刷新预览图时发生错误:\n{str(e)}")
finally:
# 恢复按钮状态
self.menu_btn.setEnabled(True)
self.menu_btn.setToolTip(original_tooltip)
def generate_project_preview(self, project_path):
"""生成项目预览图"""
try:
project_path = Path(project_path)
# 查找项目中的图片文件
image_extensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp']
found_images = []
# 搜索常见的图片目录
search_dirs = [
project_path,
project_path / 'images',
project_path / 'img',
project_path / 'assets',
project_path / 'static',
project_path / 'resources',
project_path / 'media',
project_path / 'screenshots',
project_path / 'preview'
]
for search_dir in search_dirs:
if search_dir.exists() and search_dir.is_dir():
for ext in image_extensions:
found_images.extend(search_dir.glob(f'*{ext}'))
found_images.extend(search_dir.glob(f'*{ext.upper()}'))
# 递归搜索子目录(限制深度)
found_images.extend(search_dir.glob(f'*/*{ext}'))
found_images.extend(search_dir.glob(f'*/*{ext.upper()}'))
if not found_images:
return self.create_default_preview(project_path)
# 优先选择特定名称的图片
priority_names = ['preview', 'screenshot', 'main', 'cover', 'thumbnail', 'icon']
selected_image = None
for priority_name in priority_names:
for img_path in found_images:
if priority_name in img_path.stem.lower():
selected_image = img_path
break
if selected_image:
break
# 如果没有找到优先图片,选择第一个
if not selected_image:
selected_image = found_images[0]
# 创建预览图存储目录
preview_dir = Path.cwd() / 'MetaCore' / 'Resources' / 'ProjectPreviews'
preview_dir.mkdir(parents=True, exist_ok=True)
# 生成预览图文件名
preview_filename = f"preview_{self.project.id}_{int(QDateTime.currentMSecsSinceEpoch())}.png"
preview_path = preview_dir / preview_filename
# 处理图片并保存预览图
original_pixmap = QPixmap(str(selected_image))
if not original_pixmap.isNull():
# 缩放到合适的预览尺寸
preview_size = QSize(400, 300)
scaled_pixmap = original_pixmap.scaled(
preview_size,
Qt.KeepAspectRatioByExpanding,
Qt.SmoothTransformation
)
# 裁剪到目标尺寸
if scaled_pixmap.size() != preview_size:
x = (scaled_pixmap.width() - preview_size.width()) // 2
y = (scaled_pixmap.height() - preview_size.height()) // 2
scaled_pixmap = scaled_pixmap.copy(x, y, preview_size.width(), preview_size.height())
# 保存预览图
if scaled_pixmap.save(str(preview_path), 'PNG'):
return str(preview_path)
return self.create_default_preview(project_path)
except Exception as e:
print(f"生成预览图时发生错误: {e}")
return self.create_default_preview(project_path)
def create_default_preview(self, project_path):
"""创建默认预览图"""
try:
# 创建预览图存储目录
preview_dir = Path.cwd() / 'MetaCore' / 'Resources' / 'ProjectPreviews'
preview_dir.mkdir(parents=True, exist_ok=True)
# 生成默认预览图文件名
preview_filename = f"default_preview_{self.project.id}.png"
preview_path = preview_dir / preview_filename
# 创建一个带有项目信息的默认预览图
pixmap = QPixmap(400, 300)
pixmap.fill(QColor(70, 70, 80)) # 深灰色背景
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
# 设置字体
font = QFont("微软雅黑", 16, QFont.Bold)
painter.setFont(font)
painter.setPen(QColor(255, 255, 255))
# 绘制项目名称
title_rect = QRect(20, 100, 360, 40)
painter.drawText(title_rect, Qt.AlignCenter | Qt.TextWordWrap, self.project.title)
# 绘制项目类型
type_font = QFont("微软雅黑", 12)
painter.setFont(type_font)
painter.setPen(QColor(200, 200, 200))
type_rect = QRect(20, 150, 360, 30)
type_text = self.get_type_label()
painter.drawText(type_rect, Qt.AlignCenter, f"项目类型: {type_text}")
# 绘制创建日期
date_rect = QRect(20, 180, 360, 30)
painter.drawText(date_rect, Qt.AlignCenter, f"创建时间: {self.project.date}")
# 绘制文件夹图标
icon_font = QFont("Segoe UI Emoji", 48)
painter.setFont(icon_font)
painter.setPen(QColor(150, 150, 150))
icon_rect = QRect(20, 30, 360, 60)
painter.drawText(icon_rect, Qt.AlignCenter, "📁")
painter.end()
# 保存预览图
if pixmap.save(str(preview_path), 'PNG'):
return str(preview_path)
except Exception as e:
print(f"创建默认预览图时发生错误: {e}")
return None
def refresh_image_display(self):
"""刷新图片显示"""
try:
# 更新网格视图中的图片显示
if self.view_mode == "grid":
self.update_grid_image_display()
else:
self.update_list_image_display()
except Exception as e:
print(f"刷新图片显示时发生错误: {e}")
def update_grid_image_display(self):
"""更新网格视图中的图片显示"""
# 找到图片容器
for i in range(self.layout().count()):
widget = self.layout().itemAt(i).widget()
if widget and widget.objectName() == "projectImageContainer":
# 找到内部的堆叠布局
for j in range(widget.layout().count()):
image_widget = widget.layout().itemAt(j).widget()
if image_widget and image_widget.objectName() == "projectImage":
stacked_layout = image_widget.layout()
if isinstance(stacked_layout, QStackedLayout):
# 获取图片显示控件
image_display = stacked_layout.widget(0)
if isinstance(image_display, ImageDisplayWidget):
# 检查是否有新的图片文件
if (hasattr(self.project, 'image') and
self.project.image and
os.path.exists(self.project.image) and
os.path.isfile(self.project.image)):
# 重新加载图片
pixmap = QPixmap(self.project.image)
if not pixmap.isNull():
image_display.setPixmap(pixmap)
stacked_layout.setCurrentIndex(0)
else:
stacked_layout.setCurrentIndex(1)
else:
stacked_layout.setCurrentIndex(1)
break
def update_list_image_display(self):
"""更新列表视图中的图片显示"""
# 在列表视图中查找并更新图片显示
for i in range(self.layout().count()):
widget = self.layout().itemAt(i).widget()
if widget and widget.objectName() == "listProjectIconContainer":
# 找到内部的堆叠布局
for j in range(widget.layout().count()):
icon_widget = widget.layout().itemAt(j).widget()
if icon_widget and icon_widget.objectName() == "listProjectIcon":
stacked_layout = icon_widget.layout()
if isinstance(stacked_layout, QStackedLayout):
# 获取图片显示控件
image_display = stacked_layout.widget(0)
if isinstance(image_display, ListImageDisplayWidget):
# 检查是否有新的图片文件
if (hasattr(self.project, 'image') and
self.project.image and
os.path.exists(self.project.image) and
os.path.isfile(self.project.image)):
# 重新加载图片
pixmap = QPixmap(self.project.image)
if not pixmap.isNull():
image_display.setPixmap(pixmap)
stacked_layout.setCurrentIndex(0)
else:
stacked_layout.setCurrentIndex(1)
else:
stacked_layout.setCurrentIndex(1)
break
class ListImageDisplayWidget(QWidget):
"""
专门用于列表视图的圆角图片显示控件
尺寸较小,适合列表视图
"""
def __init__(self, parent=None):
super().__init__(parent)
self.pixmap = QPixmap()
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
def setPixmap(self, pixmap):
self.pixmap = pixmap
self.update()
def paintEvent(self, event):
if self.pixmap.isNull():
return
from PyQt5.QtCore import QSize
import math
target_rect = self.rect()
radius = 8.0 # 列表视图使用较小的圆角
# 过缩放因子
overscale_factor = 1.05
original_size = target_rect.size()
larger_width = math.ceil(original_size.width() * overscale_factor)
larger_height = math.ceil(original_size.height() * overscale_factor)
larger_target_size = QSize(larger_width, larger_height)
scaled_pixmap = self.pixmap.scaled(larger_target_size, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
# 计算绘制的起始坐标,使图片居中
draw_x = (target_rect.width() - scaled_pixmap.width()) / 2
draw_y = (target_rect.height() - scaled_pixmap.height()) / 2
# 绘制
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# # 创建圆角裁剪路径
# path = QPainterPath()
# path.addRoundedRect(QRectF(target_rect), radius, radius)
# painter.setClipPath(path)
# 绘制图片
painter.drawPixmap(int(draw_x), int(draw_y), scaled_pixmap)