9fc22cc0 by 柴进

软件支持图片拖动和图片粘贴

1 parent afae62c5
......@@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.9 (GoogleNanoBananaApp) (2)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (GoogleNanoBananaApp)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (Nano_Banana_App)" project-jdk-type="Python SDK" />
</project>
\ No newline at end of file
......
......@@ -12,8 +12,8 @@ from PySide6.QtWidgets import (
QListWidget, QListWidgetItem, QTabWidget, QSplitter,
QMenu, QProgressBar
)
from PySide6.QtCore import Qt, QThread, Signal, QSize, QTimer
from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices, QAction, QImage
from PySide6.QtCore import Qt, QThread, Signal, QSize, QTimer, QMimeData
from PySide6.QtGui import QPixmap, QFont, QIcon, QDesktopServices, QAction, QImage, QDragEnterEvent, QDropEvent
from PySide6.QtCore import QUrl
import base64
......@@ -109,6 +109,7 @@ def hash_password(password: str) -> str:
class DatabaseManager:
"""数据库连接管理类"""
def __init__(self, db_config):
self.config = db_config
self.logger = logging.getLogger(__name__)
......@@ -288,7 +289,7 @@ class HistoryManager:
self.logger.debug(f"历史记录管理器初始化完成,存储路径: {self.base_path}")
def save_generation(self, image_bytes: bytes, prompt: str, reference_images: List[bytes],
aspect_ratio: str, image_size: str, model: str) -> str:
aspect_ratio: str, image_size: str, model: str) -> str:
"""保存生成的图片到历史记录
Args:
......@@ -317,7 +318,7 @@ class HistoryManager:
# 保存参考图片
reference_image_paths = []
for i, ref_img_bytes in enumerate(reference_images):
ref_path = record_dir / f"reference_{i+1}.png"
ref_path = record_dir / f"reference_{i + 1}.png"
with open(ref_path, 'wb') as f:
f.write(ref_img_bytes)
reference_image_paths.append(ref_path)
......@@ -830,6 +831,123 @@ class LoginDialog(QDialog):
return getattr(self, 'current_password_hash', '')
class DragDropScrollArea(QScrollArea):
"""自定义支持拖拽的图像滚动区域"""
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.parent_window = parent
self.setStyleSheet("""
QScrollArea {
border: 2px dashed #e5e5e5;
border-radius: 8px;
background-color: #fafafa;
}
QScrollArea:hover {
border-color: #007AFF;
background-color: #f0f8ff;
}
""")
def dragEnterEvent(self, event: QDragEnterEvent):
"""拖拽进入事件处理"""
mime_data = event.mimeData()
# 检查是否包含文件URL
if mime_data.hasUrls():
urls = mime_data.urls()
for url in urls:
if url.isLocalFile():
file_path = url.toLocalFile()
if self.is_valid_image_file(file_path):
event.acceptProposedAction()
self.setStyleSheet("""
QScrollArea {
border: 2px dashed #007AFF;
border-radius: 8px;
background-color: #e6f3ff;
}
""")
return
# 检查剪贴板图像
if mime_data.hasImage():
event.acceptProposedAction()
self.setStyleSheet("""
QScrollArea {
border: 2px dashed #007AFF;
border-radius: 8px;
background-color: #e6f3ff;
}
""")
return
event.ignore()
def dragLeaveEvent(self, event):
"""拖拽离开事件处理"""
self.setStyleSheet("""
QScrollArea {
border: 2px dashed #e5e5e5;
border-radius: 8px;
background-color: #fafafa;
}
QScrollArea:hover {
border-color: #007AFF;
background-color: #f0f8ff;
}
""")
def dropEvent(self, event: QDropEvent):
"""拖拽放置事件处理"""
mime_data = event.mimeData()
# 重置样式
self.setStyleSheet("""
QScrollArea {
border: 2px dashed #e5e5e5;
border-radius: 8px;
background-color: #fafafa;
}
QScrollArea:hover {
border-color: #007AFF;
background-color: #f0f8ff;
}
""")
# 处理文件拖拽
if mime_data.hasUrls():
urls = mime_data.urls()
image_files = []
for url in urls:
if url.isLocalFile():
file_path = url.toLocalFile()
if self.is_valid_image_file(file_path):
image_files.append(file_path)
if image_files:
self.parent_window.add_image_files(image_files)
event.acceptProposedAction()
return
# 处理剪贴板图像拖拽
if mime_data.hasImage():
image = mime_data.imageData()
if image and not image.isNull():
self.parent_window.add_clipboard_image(image)
event.acceptProposedAction()
return
event.ignore()
def is_valid_image_file(self, file_path: str) -> bool:
"""检查文件是否为有效的图像文件"""
valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
return Path(file_path).suffix.lower() in valid_extensions
class ImageGeneratorWindow(QMainWindow):
"""Qt-based main application window"""
......@@ -941,7 +1059,7 @@ class ImageGeneratorWindow(QMainWindow):
if not config_loaded and getattr(sys, 'frozen', False):
bundled_config_paths = [
Path(sys.executable).parent / 'config.json', # Same directory as exe
Path(sys._MEIPASS) / 'config.json', # PyInstaller temp directory
Path(sys._MEIPASS) / 'config.json', # PyInstaller temp directory
]
for bundled_path in bundled_config_paths:
......@@ -1013,7 +1131,8 @@ class ImageGeneratorWindow(QMainWindow):
print(f"Failed to load bundled config: {e}")
if not self.api_key:
QMessageBox.warning(self, "警告", f"未找到API密钥\n配置文件位置: {config_path}\n\n请在应用中输入API密钥或手动编辑配置文件")
QMessageBox.warning(self, "警告",
f"未找到API密钥\n配置文件位置: {config_path}\n\n请在应用中输入API密钥或手动编辑配置文件")
def save_config(self, last_user=None):
"""Save configuration to file"""
......@@ -1100,13 +1219,41 @@ class ImageGeneratorWindow(QMainWindow):
upload_btn.clicked.connect(self.upload_images)
upload_header.addWidget(upload_btn)
# Paste button
paste_btn = QPushButton("📋 粘贴图片")
paste_btn.clicked.connect(self.paste_from_clipboard)
paste_btn.setStyleSheet("""
QPushButton {
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
padding: 8px 16px;
border-radius: 4px;
font-size: 12px;
min-width: 80px;
}
QPushButton:hover {
background-color: #e8e8e8;
border-color: #007AFF;
}
QPushButton:pressed {
background-color: #d0d0d0;
}
""")
upload_header.addWidget(paste_btn)
self.image_count_label = QLabel("已选择 0 张")
upload_header.addWidget(self.image_count_label)
upload_header.addStretch()
ref_layout.addLayout(upload_header)
# Image preview scroll area
self.img_scroll = QScrollArea()
# Drag and drop hint
hint_label = QLabel("💡 提示:可以直接拖拽图片到下方区域,或使用 Ctrl+V 粘贴截图")
hint_label.setStyleSheet("QLabel { color: #666666; font-size: 11px; margin: 2px 0; }")
hint_label.setWordWrap(True)
ref_layout.addWidget(hint_label)
# Image preview scroll area with drag-and-drop support
self.img_scroll = DragDropScrollArea(self)
self.img_scroll.setWidgetResizable(True)
self.img_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.img_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
......@@ -1299,7 +1446,8 @@ class ImageGeneratorWindow(QMainWindow):
self.prompt_display = QLabel("请选择一个历史记录查看详情")
self.prompt_display.setWordWrap(True)
self.prompt_display.setStyleSheet("QLabel { padding: 8px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }")
self.prompt_display.setStyleSheet(
"QLabel { padding: 8px; background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }")
prompt_layout.addWidget(self.prompt_display)
prompt_group.setLayout(prompt_layout)
......@@ -1355,7 +1503,8 @@ class ImageGeneratorWindow(QMainWindow):
self.generated_image_label.setAlignment(Qt.AlignCenter)
self.generated_image_label.setMinimumSize(200, 200) # Larger size for generated image
self.generated_image_label.setMaximumSize(300, 300)
self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
self.generated_image_label.setStyleSheet(
"QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
self.generated_image_label.mouseDoubleClickEvent = self.open_generated_image_from_history
gen_layout.addWidget(self.generated_image_label)
......@@ -1445,6 +1594,121 @@ class ImageGeneratorWindow(QMainWindow):
self.status_label.setText(f"● 已添加 {len(files)} 张参考图片")
self.status_label.setStyleSheet("QLabel { color: #34C759; }")
def add_image_files(self, file_paths):
"""添加图像文件到上传列表(用于拖拽功能)"""
if not file_paths:
return
added_count = 0
for file_path in file_paths:
try:
if self.validate_image_file(file_path):
self.uploaded_images.append(file_path)
added_count += 1
else:
self.logger.warning(f"无效的图像文件: {file_path}")
except Exception as e:
self.logger.error(f"添加图片失败: {file_path}, 错误: {str(e)}")
if added_count > 0:
self.update_image_preview()
self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
self.status_label.setText(f"● 已通过拖拽添加 {added_count} 张参考图片")
self.status_label.setStyleSheet("QLabel { color: #34C759; }")
else:
QMessageBox.warning(self, "警告", "没有找到有效的图片文件")
def add_clipboard_image(self, image):
"""添加剪贴板图像(用于拖拽和粘贴功能)"""
try:
# 生成临时文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
temp_dir = Path(tempfile.gettempdir()) / "nano_banana_app"
temp_dir.mkdir(exist_ok=True)
# 根据图像格式选择文件扩展名
file_extension = ".png" # 默认使用PNG格式
if image.format() == QImage.Format_RGB32:
file_extension = ".bmp"
elif image.format() == QImage.Format_RGB888:
file_extension = ".jpg"
temp_file_path = temp_dir / f"clipboard_{timestamp}{file_extension}"
# 保存图像到临时文件
if image.save(str(temp_file_path)):
self.uploaded_images.append(str(temp_file_path))
self.update_image_preview()
self.image_count_label.setText(f"已选择 {len(self.uploaded_images)} 张")
self.status_label.setText("● 已添加剪贴板图片")
self.status_label.setStyleSheet("QLabel { color: #34C759; }")
self.logger.info(f"剪贴板图片已保存到: {temp_file_path}")
else:
QMessageBox.critical(self, "错误", "无法保存剪贴板图片")
except Exception as e:
self.logger.error(f"添加剪贴板图片失败: {str(e)}")
QMessageBox.critical(self, "错误", f"添加剪贴板图片失败: {str(e)}")
def paste_from_clipboard(self):
"""从剪贴板粘贴图像"""
clipboard = QApplication.clipboard()
# 检查剪贴板中是否有图像
if clipboard.mimeData().hasImage():
image = clipboard.image()
if not image.isNull():
self.add_clipboard_image(image)
else:
QMessageBox.information(self, "信息", "剪贴板中没有有效的图片")
else:
QMessageBox.information(self, "信息", "剪贴板中没有图片,请先复制一张图片")
def validate_image_file(self, file_path: str) -> bool:
"""验证图像文件"""
try:
# 检查文件是否存在
if not Path(file_path).exists():
return False
# 检查文件扩展名
valid_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
if Path(file_path).suffix.lower() not in valid_extensions:
return False
# 尝试加载图像以验证文件完整性
pixmap = QPixmap(file_path)
if pixmap.isNull():
return False
# 检查文件大小(限制为10MB)
file_size = Path(file_path).stat().st_size
if file_size > 10 * 1024 * 1024: # 10MB
QMessageBox.warning(self, "警告", f"图片文件过大: {file_path}\n请选择小于10MB的图片")
return False
return True
except Exception as e:
self.logger.error(f"图像文件验证失败: {file_path}, 错误: {str(e)}")
return False
def keyPressEvent(self, event):
"""处理键盘事件"""
# Ctrl+V 粘贴
if event.key() == Qt.Key_V and event.modifiers() == Qt.ControlModifier:
self.paste_from_clipboard()
event.accept()
return
# Cmd+V 粘贴 (macOS)
elif event.key() == Qt.Key_V and event.modifiers() == Qt.MetaModifier:
self.paste_from_clipboard()
event.accept()
return
super().keyPressEvent(event)
def update_image_preview(self):
"""Update image preview thumbnails"""
# Clear existing previews
......@@ -1707,7 +1971,6 @@ class ImageGeneratorWindow(QMainWindow):
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
def refresh_history(self):
"""Refresh the history list"""
self.history_list.clear()
......@@ -1954,18 +2217,22 @@ class ImageGeneratorWindow(QMainWindow):
Qt.SmoothTransformation
)
self.generated_image_label.setPixmap(scaled_pixmap)
self.generated_image_label.setStyleSheet("QLabel { border: 1px solid #ddd; background-color: white; }")
self.generated_image_label.setStyleSheet(
"QLabel { border: 1px solid #ddd; background-color: white; }")
self.current_generated_image_path = image_path
else:
self.generated_image_label.setText("图片加载失败")
self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
self.generated_image_label.setStyleSheet(
"QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
except Exception as e:
print(f"Failed to load generated image {image_path}: {e}")
self.generated_image_label.setText("图片加载失败")
self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
self.generated_image_label.setStyleSheet(
"QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
else:
self.generated_image_label.setText("图片文件不存在")
self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
self.generated_image_label.setStyleSheet(
"QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
def clear_details_panel(self):
"""Clear the details panel"""
......@@ -1986,7 +2253,8 @@ class ImageGeneratorWindow(QMainWindow):
self.generated_image_label.setText("请选择一个历史记录查看生成图片")
self.generated_image_label.setPixmap(QPixmap())
self.generated_image_label.setStyleSheet("QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
self.generated_image_label.setStyleSheet(
"QLabel { background-color: #f5f5f5; border: 1px solid #ddd; color: #999; }")
def copy_prompt_text(self):
"""Copy the prompt text to clipboard"""
......@@ -2027,7 +2295,8 @@ class ImageGeneratorWindow(QMainWindow):
class ImageGenerationWorker(QThread):
"""Worker thread for image generation"""
finished = Signal(bytes, str, list, str, str, str) # image_bytes, prompt, reference_images, aspect_ratio, image_size, model
finished = Signal(bytes, str, list, str, str,
str) # image_bytes, prompt, reference_images, aspect_ratio, image_size, model
error = Signal(str)
progress = Signal(str)
......@@ -2119,7 +2388,7 @@ class ImageGenerationWorker(QThread):
self.logger.info(f"图片生成成功 - 模型: {self.model}, 尺寸: {self.image_size}")
self.finished.emit(image_bytes, self.prompt, reference_images_bytes,
self.aspect_ratio, self.image_size, self.model)
self.aspect_ratio, self.image_size, self.model)
return
error_msg = "响应中没有图片数据"
......