2025-09-04 12:55:33 +08:00
|
|
|
|
"""
|
|
|
|
|
|
作者:太一
|
|
|
|
|
|
微信:taiyi1224
|
|
|
|
|
|
邮箱:shuobo1224@qq.com
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2025-09-02 16:49:39 +08:00
|
|
|
|
import tkinter as tk
|
|
|
|
|
|
from tkinter import ttk, messagebox, filedialog, simpledialog
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from models import Project, Template
|
|
|
|
|
|
from canvas_widget import CanvasWidget
|
|
|
|
|
|
from template_dialog import TemplateDialog
|
|
|
|
|
|
from export_dialog import ExportDialog
|
|
|
|
|
|
from exporter import Exporter
|
|
|
|
|
|
|
|
|
|
|
|
# 确保中文显示正常
|
|
|
|
|
|
import matplotlib
|
|
|
|
|
|
|
|
|
|
|
|
matplotlib.use('Agg')
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
|
|
|
|
|
|
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MainWindow(tk.Tk):
|
|
|
|
|
|
"""主窗口"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.title("批量图片布局工具")
|
|
|
|
|
|
self.project = Project()
|
|
|
|
|
|
self.init_ui()
|
|
|
|
|
|
|
|
|
|
|
|
def init_ui(self):
|
|
|
|
|
|
"""初始化UI"""
|
|
|
|
|
|
# 设置样式
|
|
|
|
|
|
style = ttk.Style()
|
|
|
|
|
|
style.configure("TButton", padding=5)
|
|
|
|
|
|
style.configure("TLabel", padding=2)
|
|
|
|
|
|
|
|
|
|
|
|
# 菜单栏
|
|
|
|
|
|
self.create_menu()
|
|
|
|
|
|
|
|
|
|
|
|
# 工具栏
|
|
|
|
|
|
toolbar = ttk.Frame(self, padding=5)
|
|
|
|
|
|
toolbar.pack(fill=tk.X)
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Button(toolbar, text="打开背景", command=self.open_background).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
ttk.Button(toolbar, text="添加图片", command=self.add_foregrounds).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
ttk.Button(toolbar, text="清除图片", command=self.clear_foregrounds).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
ttk.Button(toolbar, text="模板设置", command=self.set_template).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
ttk.Button(toolbar, text="批量导出", command=self.batch_export).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
|
|
|
|
|
|
# 主布局
|
|
|
|
|
|
main_frame = ttk.Frame(self, padding=5)
|
|
|
|
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 左侧:图片列表
|
|
|
|
|
|
left_frame = ttk.Frame(main_frame, width=200)
|
|
|
|
|
|
left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5))
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(left_frame, text="前景图片列表", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
self.image_list_frame = ttk.Frame(left_frame)
|
|
|
|
|
|
self.image_list_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 滚动条
|
|
|
|
|
|
scrollbar = ttk.Scrollbar(self.image_list_frame)
|
|
|
|
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
|
|
|
|
|
|
|
|
self.image_listbox = tk.Listbox(
|
|
|
|
|
|
self.image_list_frame,
|
|
|
|
|
|
yscrollcommand=scrollbar.set,
|
|
|
|
|
|
selectmode=tk.SINGLE,
|
|
|
|
|
|
height=20
|
|
|
|
|
|
)
|
|
|
|
|
|
self.image_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
|
|
scrollbar.config(command=self.image_listbox.yview)
|
|
|
|
|
|
|
|
|
|
|
|
# 绑定列表事件
|
|
|
|
|
|
self.image_listbox.bind('<<ListboxSelect>>', self.on_image_select)
|
|
|
|
|
|
|
|
|
|
|
|
# 添加按钮
|
|
|
|
|
|
ttk.Button(left_frame, text="添加图片", command=self.add_foregrounds).pack(fill=tk.X, pady=5)
|
|
|
|
|
|
ttk.Button(left_frame, text="移除选中", command=self.remove_selected).pack(fill=tk.X)
|
|
|
|
|
|
|
|
|
|
|
|
# 中间:画布区域
|
|
|
|
|
|
center_frame = ttk.Frame(main_frame)
|
|
|
|
|
|
center_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 画布滚动条
|
|
|
|
|
|
hscroll = ttk.Scrollbar(center_frame, orient=tk.HORIZONTAL)
|
|
|
|
|
|
hscroll.pack(side=tk.BOTTOM, fill=tk.X)
|
|
|
|
|
|
|
|
|
|
|
|
vscroll = ttk.Scrollbar(center_frame, orient=tk.VERTICAL)
|
|
|
|
|
|
vscroll.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建画布
|
|
|
|
|
|
self.canvas = CanvasWidget(center_frame, self.project, main_window=self)
|
|
|
|
|
|
# self.canvas = CanvasWidget(center_frame, self.project)
|
|
|
|
|
|
self.canvas.pack(fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 关联滚动条
|
|
|
|
|
|
self.canvas.config(xscrollcommand=hscroll.set, yscrollcommand=vscroll.set)
|
|
|
|
|
|
hscroll.config(command=self.canvas.xview)
|
|
|
|
|
|
vscroll.config(command=self.canvas.yview)
|
|
|
|
|
|
|
|
|
|
|
|
# 右侧:属性面板
|
|
|
|
|
|
right_frame = ttk.Frame(main_frame, width=200)
|
|
|
|
|
|
right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(5, 0))
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(right_frame, text="属性设置", font=("Arial", 10, "bold")).pack(anchor=tk.W, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
self.property_frame = ttk.Frame(right_frame)
|
|
|
|
|
|
self.property_frame.pack(fill=tk.BOTH, expand=True, padx=5)
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化属性面板内容
|
|
|
|
|
|
self.clear_property_panel()
|
|
|
|
|
|
|
|
|
|
|
|
# 设置窗口大小
|
|
|
|
|
|
self.geometry("1200x700")
|
|
|
|
|
|
|
|
|
|
|
|
def create_menu(self):
|
|
|
|
|
|
"""创建菜单栏"""
|
|
|
|
|
|
menubar = tk.Menu(self)
|
|
|
|
|
|
|
|
|
|
|
|
# 文件菜单
|
|
|
|
|
|
file_menu = tk.Menu(menubar, tearoff=0)
|
|
|
|
|
|
file_menu.add_command(label="新建项目", command=self.new_project)
|
|
|
|
|
|
file_menu.add_command(label="打开项目", command=self.open_project)
|
|
|
|
|
|
file_menu.add_command(label="保存项目", command=self.save_project)
|
|
|
|
|
|
file_menu.add_command(label="另存为", command=self.save_project_as)
|
|
|
|
|
|
file_menu.add_separator()
|
|
|
|
|
|
file_menu.add_command(label="退出", command=self.quit)
|
|
|
|
|
|
menubar.add_cascade(label="文件", menu=file_menu)
|
|
|
|
|
|
|
|
|
|
|
|
# 编辑菜单
|
|
|
|
|
|
edit_menu = tk.Menu(menubar, tearoff=0)
|
|
|
|
|
|
edit_menu.add_command(label="清除所有图片", command=self.clear_foregrounds)
|
|
|
|
|
|
edit_menu.add_separator()
|
|
|
|
|
|
edit_menu.add_command(label="上移图层", command=lambda: self.move_layer(1))
|
|
|
|
|
|
edit_menu.add_command(label="下移图层", command=lambda: self.move_layer(-1))
|
|
|
|
|
|
menubar.add_cascade(label="编辑", menu=edit_menu)
|
|
|
|
|
|
|
|
|
|
|
|
# 视图菜单
|
|
|
|
|
|
view_menu = tk.Menu(menubar, tearoff=0)
|
|
|
|
|
|
view_menu.add_command(label="缩放重置", command=self.reset_zoom)
|
|
|
|
|
|
menubar.add_cascade(label="视图", menu=view_menu)
|
|
|
|
|
|
|
|
|
|
|
|
# 帮助菜单
|
|
|
|
|
|
help_menu = tk.Menu(menubar, tearoff=0)
|
|
|
|
|
|
help_menu.add_command(label="关于", command=self.show_about)
|
|
|
|
|
|
menubar.add_cascade(label="帮助", menu=help_menu)
|
|
|
|
|
|
|
|
|
|
|
|
# 设置菜单栏
|
|
|
|
|
|
self.config(menu=menubar)
|
|
|
|
|
|
|
|
|
|
|
|
def open_background(self):
|
|
|
|
|
|
"""打开背景图"""
|
|
|
|
|
|
file_path = filedialog.askopenfilename(
|
|
|
|
|
|
filetypes=[
|
|
|
|
|
|
("图像文件", "*.jpg;*.jpeg;*.png;*.webp;*.bmp"),
|
|
|
|
|
|
("所有文件", "*.*")
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
if file_path:
|
|
|
|
|
|
if self.project.template.load_background(file_path):
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def add_foregrounds(self):
|
|
|
|
|
|
"""添加前景图"""
|
|
|
|
|
|
file_paths = filedialog.askopenfilenames(
|
|
|
|
|
|
filetypes=[
|
|
|
|
|
|
("图像文件", "*.jpg;*.jpeg;*.png;*.webp;*.bmp"),
|
|
|
|
|
|
("所有文件", "*.*")
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if file_paths:
|
|
|
|
|
|
# 检查是否是第一次添加图片
|
|
|
|
|
|
is_first_batch = len(self.project.foregrounds) == 0
|
2025-09-04 12:55:33 +08:00
|
|
|
|
|
2025-09-02 16:49:39 +08:00
|
|
|
|
# 添加所有选择的图片
|
|
|
|
|
|
added_foregrounds = []
|
|
|
|
|
|
for file_path in file_paths:
|
|
|
|
|
|
# 检查是否已添加
|
|
|
|
|
|
if not any(fg.file_path == file_path for fg in self.project.foregrounds):
|
|
|
|
|
|
fg = self.project.add_foreground(file_path)
|
|
|
|
|
|
added_foregrounds.append(fg)
|
2025-09-04 12:55:33 +08:00
|
|
|
|
|
2025-09-02 16:49:39 +08:00
|
|
|
|
# 如果是第一次批量添加,则设置批量处理模式
|
|
|
|
|
|
if is_first_batch and len(added_foregrounds) > 1:
|
|
|
|
|
|
# 第一张图片保持可见和可编辑
|
|
|
|
|
|
first_fg = added_foregrounds[0]
|
|
|
|
|
|
first_fg.hidden = False
|
|
|
|
|
|
first_fg.visible = True
|
2025-09-04 12:55:33 +08:00
|
|
|
|
|
2025-09-02 16:49:39 +08:00
|
|
|
|
# 其他图片设置为隐藏
|
|
|
|
|
|
for fg in added_foregrounds[1:]:
|
|
|
|
|
|
fg.hidden = True
|
|
|
|
|
|
fg.visible = False
|
2025-09-04 12:55:33 +08:00
|
|
|
|
|
2025-09-02 16:49:39 +08:00
|
|
|
|
# 选中第一张图片
|
|
|
|
|
|
self.canvas.selected_item = first_fg
|
|
|
|
|
|
self.update_property_panel(first_fg)
|
|
|
|
|
|
elif not is_first_batch and self.project.foregrounds:
|
|
|
|
|
|
# 如果不是第一次添加,且已存在图片,则复制第一张图片的配置
|
|
|
|
|
|
reference_fg = self.project.foregrounds[0]
|
|
|
|
|
|
for fg in added_foregrounds:
|
|
|
|
|
|
fg.vertices = [tuple(v) for v in reference_fg.vertices]
|
|
|
|
|
|
fg.angle = reference_fg.angle
|
|
|
|
|
|
fg.mask_shape = reference_fg.mask_shape
|
|
|
|
|
|
fg.locked = reference_fg.locked
|
|
|
|
|
|
fg.visible = reference_fg.visible
|
|
|
|
|
|
fg.hidden = reference_fg.hidden
|
|
|
|
|
|
|
|
|
|
|
|
# 更新列表
|
|
|
|
|
|
self.update_image_list()
|
|
|
|
|
|
# 重绘画布
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def update_image_list(self):
|
|
|
|
|
|
"""更新图片列表"""
|
|
|
|
|
|
self.image_listbox.delete(0, tk.END)
|
|
|
|
|
|
for fg in self.project.foregrounds:
|
|
|
|
|
|
name = Path(fg.file_path).name
|
|
|
|
|
|
self.image_listbox.insert(tk.END, name)
|
|
|
|
|
|
|
|
|
|
|
|
def on_image_select(self, event):
|
|
|
|
|
|
"""处理列表选择事件"""
|
|
|
|
|
|
selection = self.image_listbox.curselection()
|
|
|
|
|
|
if selection:
|
|
|
|
|
|
index = selection[0]
|
|
|
|
|
|
if 0 <= index < len(self.project.foregrounds):
|
|
|
|
|
|
fg = self.project.foregrounds[index]
|
|
|
|
|
|
self.canvas.selected_item = fg
|
|
|
|
|
|
self.update_property_panel(fg)
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果没有选择任何项目,显示默认面板
|
|
|
|
|
|
self.clear_property_panel()
|
|
|
|
|
|
|
|
|
|
|
|
def remove_selected(self):
|
|
|
|
|
|
"""移除选中的图片"""
|
|
|
|
|
|
selection = self.image_listbox.curselection()
|
|
|
|
|
|
if selection:
|
|
|
|
|
|
index = selection[0]
|
|
|
|
|
|
if 0 <= index < len(self.project.foregrounds):
|
|
|
|
|
|
fg = self.project.foregrounds[index]
|
|
|
|
|
|
self.project.remove_foreground(fg)
|
|
|
|
|
|
self.update_image_list()
|
|
|
|
|
|
self.clear_property_panel()
|
|
|
|
|
|
self.canvas.selected_item = None
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def clear_foregrounds(self):
|
|
|
|
|
|
"""清除所有前景图"""
|
|
|
|
|
|
if messagebox.askyesno("确认", "确定要清除所有前景图片吗?"):
|
|
|
|
|
|
self.project.foregrounds.clear()
|
|
|
|
|
|
self.update_image_list()
|
|
|
|
|
|
self.clear_property_panel()
|
|
|
|
|
|
self.canvas.selected_item = None
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def get_canvas(self):
|
|
|
|
|
|
"""获取画布组件"""
|
|
|
|
|
|
return self.canvas
|
|
|
|
|
|
|
|
|
|
|
|
def set_template(self):
|
|
|
|
|
|
"""设置模板"""
|
|
|
|
|
|
dialog = TemplateDialog(self, self.project.template)
|
|
|
|
|
|
if dialog.result:
|
2025-09-04 12:55:33 +08:00
|
|
|
|
# 处理所有类型的模板设置结果
|
|
|
|
|
|
if dialog.result["type"] == "preset_loaded":
|
|
|
|
|
|
# 直接更新现有模板的属性
|
|
|
|
|
|
self.project.template.ratio = dialog.result["ratio"]
|
|
|
|
|
|
self.project.template.width_px = dialog.result["width"]
|
|
|
|
|
|
self.project.template.height_px = dialog.result["height"]
|
|
|
|
|
|
self.project.template.bg_color = dialog.result["bg_color"]
|
|
|
|
|
|
if "bg_image_path" in dialog.result and dialog.result["bg_image_path"]:
|
|
|
|
|
|
self.project.template.load_background(dialog.result["bg_image_path"])
|
|
|
|
|
|
elif dialog.result["type"] == "preset":
|
|
|
|
|
|
# 更新现有模板的属性
|
|
|
|
|
|
self.project.template.ratio = tuple(dialog.result["ratio"])
|
|
|
|
|
|
self.project.template.width_px = dialog.result["width"]
|
|
|
|
|
|
self.project.template.height_px = dialog.result["height"]
|
|
|
|
|
|
self.project.template.bg_color = dialog.result["bg_color"]
|
|
|
|
|
|
# 设置背景图
|
|
|
|
|
|
if "bg_image_path" in dialog.result and dialog.result["bg_image_path"]:
|
|
|
|
|
|
self.project.template.load_background(dialog.result["bg_image_path"])
|
|
|
|
|
|
elif "bg_image" in dialog.result:
|
|
|
|
|
|
self.project.template.bg_image = dialog.result["bg_image"]
|
|
|
|
|
|
elif dialog.result["type"] == "custom":
|
|
|
|
|
|
# 更新现有模板的属性
|
2025-09-02 16:49:39 +08:00
|
|
|
|
self.project.template.set_custom_size(
|
|
|
|
|
|
dialog.result["width"],
|
|
|
|
|
|
dialog.result["height"]
|
|
|
|
|
|
)
|
2025-09-04 12:55:33 +08:00
|
|
|
|
# 设置ratio属性
|
|
|
|
|
|
if "ratio" in dialog.result:
|
|
|
|
|
|
self.project.template.ratio = dialog.result["ratio"]
|
|
|
|
|
|
self.project.template.bg_color = dialog.result["bg_color"]
|
|
|
|
|
|
# 设置背景图
|
|
|
|
|
|
if "bg_image_path" in dialog.result and dialog.result["bg_image_path"]:
|
|
|
|
|
|
self.project.template.load_background(dialog.result["bg_image_path"])
|
|
|
|
|
|
elif "bg_image" in dialog.result:
|
|
|
|
|
|
self.project.template.bg_image = dialog.result["bg_image"]
|
|
|
|
|
|
|
|
|
|
|
|
# 确保更新画布显示
|
2025-09-02 16:49:39 +08:00
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def batch_export(self):
|
|
|
|
|
|
"""批量导出"""
|
|
|
|
|
|
if not self.project.foregrounds:
|
|
|
|
|
|
messagebox.showinfo("提示", "请先添加前景图片")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 在导出前,将第一张图片的配置应用到所有隐藏的图片
|
|
|
|
|
|
if self.project.foregrounds:
|
|
|
|
|
|
first_fg = self.project.foregrounds[0]
|
|
|
|
|
|
# 将第一张图片的配置应用到所有隐藏的图片
|
|
|
|
|
|
for fg in self.project.foregrounds[1:]:
|
|
|
|
|
|
if fg.hidden:
|
|
|
|
|
|
fg.vertices = [tuple(v) for v in first_fg.vertices]
|
|
|
|
|
|
fg.angle = first_fg.angle
|
|
|
|
|
|
fg.mask_shape = first_fg.mask_shape
|
|
|
|
|
|
|
|
|
|
|
|
# 设置默认导出路径为第一个图片所在目录下的output文件夹
|
|
|
|
|
|
default_output_dir = None
|
|
|
|
|
|
if self.project.foregrounds:
|
|
|
|
|
|
first_image_dir = Path(self.project.foregrounds[0].file_path).parent
|
|
|
|
|
|
default_output_dir = first_image_dir / "output"
|
2025-09-04 12:55:33 +08:00
|
|
|
|
|
2025-09-02 16:49:39 +08:00
|
|
|
|
dialog = ExportDialog(self, len(self.project.foregrounds))
|
|
|
|
|
|
if dialog.result:
|
|
|
|
|
|
# 禁用主窗口防止重复操作
|
|
|
|
|
|
self.grab_set()
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 执行导出
|
|
|
|
|
|
success = Exporter.export_batch(
|
|
|
|
|
|
self.project,
|
|
|
|
|
|
dialog.output_dir_var.get(),
|
|
|
|
|
|
dialog.filename_var.get(),
|
|
|
|
|
|
dialog.format_var.get(),
|
|
|
|
|
|
dialog.quality_var.get(),
|
|
|
|
|
|
progress_callback=self.update_export_progress
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 导出完成
|
|
|
|
|
|
if success:
|
|
|
|
|
|
messagebox.showinfo("成功", f"已成功导出 {len(self.project.foregrounds)} 张图片")
|
|
|
|
|
|
else:
|
|
|
|
|
|
messagebox.showerror("错误", "导出过程中发生错误")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# 重新启用主窗口
|
|
|
|
|
|
self.grab_release()
|
|
|
|
|
|
|
|
|
|
|
|
def update_export_progress(self, current, total):
|
|
|
|
|
|
"""更新导出进度(这个方法需要在ExportDialog中实现)"""
|
|
|
|
|
|
# 注意:这个方法需要在ExportDialog中调用,而不是直接在这里实现
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def update_property_panel(self, fg):
|
|
|
|
|
|
"""更新属性面板"""
|
|
|
|
|
|
# 清除现有内容
|
|
|
|
|
|
for widget in self.property_frame.winfo_children():
|
|
|
|
|
|
widget.destroy()
|
|
|
|
|
|
|
|
|
|
|
|
# 位置设置
|
|
|
|
|
|
ttk.Label(self.property_frame, text="位置", font=("Arial", 9, "bold")).pack(anchor=tk.W, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
pos_frame = ttk.Frame(self.property_frame)
|
|
|
|
|
|
pos_frame.pack(fill=tk.X, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(pos_frame, text="X:").grid(row=0, column=0, sticky=tk.W)
|
|
|
|
|
|
x_var = tk.DoubleVar(value=fg.x)
|
|
|
|
|
|
x_entry = ttk.Entry(pos_frame, textvariable=x_var, width=10)
|
|
|
|
|
|
x_entry.grid(row=0, column=1, padx=2)
|
|
|
|
|
|
x_entry.bind("<FocusOut>", lambda e: self.update_property(fg, "x", x_var.get()))
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(pos_frame, text="Y:").grid(row=0, column=2, sticky=tk.W, padx=5)
|
|
|
|
|
|
y_var = tk.DoubleVar(value=fg.y)
|
|
|
|
|
|
y_entry = ttk.Entry(pos_frame, textvariable=y_var, width=10)
|
|
|
|
|
|
y_entry.grid(row=0, column=3, padx=2)
|
|
|
|
|
|
y_entry.bind("<FocusOut>", lambda e: self.update_property(fg, "y", y_var.get()))
|
|
|
|
|
|
|
|
|
|
|
|
# 尺寸设置
|
|
|
|
|
|
ttk.Label(self.property_frame, text="尺寸", font=("Arial", 9, "bold")).pack(anchor=tk.W, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
size_frame = ttk.Frame(self.property_frame)
|
|
|
|
|
|
size_frame.pack(fill=tk.X, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(size_frame, text="宽:").grid(row=0, column=0, sticky=tk.W)
|
|
|
|
|
|
w_var = tk.DoubleVar(value=fg.w)
|
|
|
|
|
|
w_entry = ttk.Entry(size_frame, textvariable=w_var, width=10)
|
|
|
|
|
|
w_entry.grid(row=0, column=1, padx=2)
|
|
|
|
|
|
w_entry.bind("<FocusOut>", lambda e: self.update_property(fg, "w", w_var.get()))
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(size_frame, text="高:").grid(row=0, column=2, sticky=tk.W, padx=5)
|
|
|
|
|
|
h_var = tk.DoubleVar(value=fg.h)
|
|
|
|
|
|
h_entry = ttk.Entry(size_frame, textvariable=h_var, width=10)
|
|
|
|
|
|
h_entry.grid(row=0, column=3, padx=2)
|
|
|
|
|
|
h_entry.bind("<FocusOut>", lambda e: self.update_property(fg, "h", h_var.get()))
|
|
|
|
|
|
|
|
|
|
|
|
# 旋转设置
|
|
|
|
|
|
ttk.Label(self.property_frame, text="旋转", font=("Arial", 9, "bold")).pack(anchor=tk.W, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
rot_frame = ttk.Frame(self.property_frame)
|
|
|
|
|
|
rot_frame.pack(fill=tk.X, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
angle_var = tk.DoubleVar(value=fg.angle)
|
|
|
|
|
|
angle_entry = ttk.Entry(rot_frame, textvariable=angle_var, width=10)
|
|
|
|
|
|
angle_entry.grid(row=0, column=0, padx=2)
|
|
|
|
|
|
ttk.Label(rot_frame, text="度").grid(row=0, column=1, sticky=tk.W)
|
|
|
|
|
|
angle_entry.bind("<FocusOut>", lambda e: self.update_property(fg, "angle", angle_var.get()))
|
|
|
|
|
|
|
|
|
|
|
|
# 遮罩设置
|
|
|
|
|
|
ttk.Label(self.property_frame, text="遮罩", font=("Arial", 9, "bold")).pack(anchor=tk.W, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
mask_frame = ttk.Frame(self.property_frame)
|
|
|
|
|
|
mask_frame.pack(fill=tk.X, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
mask_var = tk.StringVar(value=fg.mask_shape)
|
|
|
|
|
|
masks = [("矩形", "rect"), ("圆形", "round"), ("心形", "heart"), ("星形", "star")]
|
|
|
|
|
|
for i, (text, value) in enumerate(masks):
|
|
|
|
|
|
ttk.Radiobutton(
|
|
|
|
|
|
mask_frame,
|
|
|
|
|
|
text=text,
|
|
|
|
|
|
variable=mask_var,
|
|
|
|
|
|
value=value,
|
|
|
|
|
|
command=lambda: self.update_property(fg, "mask_shape", mask_var.get())
|
|
|
|
|
|
).grid(row=i // 2, column=i % 2, sticky=tk.W, padx=2, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
# 其他设置
|
|
|
|
|
|
ttk.Label(self.property_frame, text="其他", font=("Arial", 9, "bold")).pack(anchor=tk.W, pady=5)
|
|
|
|
|
|
|
|
|
|
|
|
lock_var = tk.BooleanVar(value=fg.locked)
|
|
|
|
|
|
ttk.Checkbutton(
|
|
|
|
|
|
self.property_frame,
|
|
|
|
|
|
text="锁定位置",
|
|
|
|
|
|
variable=lock_var,
|
|
|
|
|
|
command=lambda: self.update_property(fg, "locked", lock_var.get())
|
|
|
|
|
|
).pack(anchor=tk.W, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
visible_var = tk.BooleanVar(value=fg.visible)
|
|
|
|
|
|
ttk.Checkbutton(
|
|
|
|
|
|
self.property_frame,
|
|
|
|
|
|
text="显示图片",
|
|
|
|
|
|
variable=visible_var,
|
|
|
|
|
|
command=lambda: self.update_property(fg, "visible", visible_var.get())
|
|
|
|
|
|
).pack(anchor=tk.W, pady=2)
|
|
|
|
|
|
|
|
|
|
|
|
def update_property(self, fg, prop, value):
|
|
|
|
|
|
"""更新属性值"""
|
|
|
|
|
|
if hasattr(fg, prop):
|
|
|
|
|
|
setattr(fg, prop, value)
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def clear_property_panel(self):
|
|
|
|
|
|
"""清除属性面板"""
|
|
|
|
|
|
for widget in self.property_frame.winfo_children():
|
|
|
|
|
|
widget.destroy()
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(self.property_frame, text="未选择图片", foreground="#999").pack(pady=20)
|
|
|
|
|
|
|
|
|
|
|
|
def move_layer(self, direction):
|
|
|
|
|
|
"""移动图层顺序"""
|
|
|
|
|
|
if not self.canvas.selected_item:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
fg = self.canvas.selected_item
|
|
|
|
|
|
current_idx = fg.z_index
|
|
|
|
|
|
new_idx = current_idx + direction
|
|
|
|
|
|
|
|
|
|
|
|
# 检查边界
|
|
|
|
|
|
if new_idx < 0 or new_idx >= len(self.project.foregrounds):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 交换z_index
|
|
|
|
|
|
for other in self.project.foregrounds:
|
|
|
|
|
|
if other.z_index == new_idx:
|
|
|
|
|
|
other.z_index = current_idx
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
fg.z_index = new_idx
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def reset_zoom(self):
|
|
|
|
|
|
"""重置缩放"""
|
|
|
|
|
|
self.project.canvas_zoom = 1.0
|
|
|
|
|
|
self.canvas.zoom = 1.0
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def new_project(self):
|
|
|
|
|
|
"""新建项目"""
|
|
|
|
|
|
if messagebox.askyesno("确认", "确定要创建新项目吗?当前项目将被清空。"):
|
|
|
|
|
|
self.project = Project()
|
|
|
|
|
|
self.canvas.project = self.project
|
|
|
|
|
|
self.update_image_list()
|
|
|
|
|
|
self.clear_property_panel()
|
|
|
|
|
|
self.canvas.selected_item = None
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
|
|
|
|
|
|
def open_project(self):
|
|
|
|
|
|
"""打开项目"""
|
|
|
|
|
|
file_path = filedialog.askopenfilename(
|
|
|
|
|
|
filetypes=[("项目文件", "*.json"), ("所有文件", "*.*")]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if file_path:
|
|
|
|
|
|
new_project = Project()
|
|
|
|
|
|
if new_project.load(file_path):
|
|
|
|
|
|
self.project = new_project
|
|
|
|
|
|
self.canvas.project = self.project
|
|
|
|
|
|
self.canvas.zoom = self.project.canvas_zoom
|
|
|
|
|
|
self.update_image_list()
|
|
|
|
|
|
self.clear_property_panel()
|
|
|
|
|
|
self.canvas.selected_item = None
|
|
|
|
|
|
self.canvas.draw_preview()
|
|
|
|
|
|
else:
|
|
|
|
|
|
messagebox.showerror("错误", "打开项目失败")
|
|
|
|
|
|
|
|
|
|
|
|
def save_project(self):
|
|
|
|
|
|
"""保存项目"""
|
|
|
|
|
|
if self.project.file_path:
|
|
|
|
|
|
if self.project.save():
|
|
|
|
|
|
messagebox.showinfo("成功", "项目保存成功")
|
|
|
|
|
|
else:
|
|
|
|
|
|
messagebox.showerror("错误", "项目保存失败")
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.save_project_as()
|
|
|
|
|
|
|
|
|
|
|
|
def save_project_as(self):
|
|
|
|
|
|
"""另存为项目"""
|
|
|
|
|
|
file_path = filedialog.asksaveasfilename(
|
|
|
|
|
|
defaultextension=".json",
|
|
|
|
|
|
filetypes=[("项目文件", "*.json"), ("所有文件", "*.*")]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if file_path:
|
|
|
|
|
|
if self.project.save(file_path):
|
|
|
|
|
|
messagebox.showinfo("成功", "项目保存成功")
|
|
|
|
|
|
else:
|
|
|
|
|
|
messagebox.showerror("错误", "项目保存失败")
|
|
|
|
|
|
|
|
|
|
|
|
def show_about(self):
|
|
|
|
|
|
"""显示关于对话框"""
|
|
|
|
|
|
messagebox.showinfo(
|
|
|
|
|
|
"关于",
|
|
|
|
|
|
"批量图片布局工具\n"
|
|
|
|
|
|
"版本: 1.0\n"
|
|
|
|
|
|
"用于批量处理图片布局的工具,可以将多张图片按照相同的布局应用到指定模板中。"
|
|
|
|
|
|
)
|