Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
柴进
/
GoogleNanoBananaApp
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
9fc22cc0
authored
2025-12-03 17:13:25 +0800
by
柴进
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
软件支持图片拖动和图片粘贴
1 parent
afae62c5
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
285 additions
and
16 deletions
.idea/misc.xml
image_generator.py
.idea/misc.xml
View file @
9fc22cc
...
...
@@ -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 (
GoogleNanoBanana
App)"
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
...
...
image_generator.py
View file @
9fc22cc
...
...
@@ -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__
)
...
...
@@ -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"""
...
...
@@ -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
)
...
...
Please
register
or
sign in
to post a comment