|
2 | 2 | import os
|
3 | 3 | from PIL import Image, ImageTk, ImageSequence
|
4 | 4 | import tkinter as tk
|
5 |
| -from tkinter import filedialog, messagebox |
6 |
| -from tkinter import ttk |
| 5 | +from tkinter import filedialog, messagebox, ttk |
7 | 6 |
|
8 | 7 | class SpriteSheetGenerator:
|
9 | 8 | def __init__(self, cell_width=128, cell_height=128, rows=8, cols=8):
|
10 |
| - self.cell_width = cell_width # 单元格宽度 |
11 |
| - self.cell_height = cell_height # 单元格高度 |
12 |
| - self.rows = rows # 行数 |
13 |
| - self.cols = cols # 列数 |
| 9 | + self.cell_width = cell_width |
| 10 | + self.cell_height = cell_height |
| 11 | + self.rows = rows |
| 12 | + self.cols = cols |
14 | 13 |
|
15 |
| - def generate_spritesheet(self, gif_path, output_file): |
| 14 | + def generate(self, gif_path, output_file): |
16 | 15 | try:
|
17 | 16 | gif = Image.open(gif_path)
|
18 |
| - frames = [frame.copy() for frame in ImageSequence.Iterator(gif)] |
19 |
| - total_frames = len(frames) |
| 17 | + frames = [] |
20 | 18 |
|
21 |
| - if total_frames > self.rows * self.cols: |
22 |
| - messagebox.showwarning( |
23 |
| - "帧数超限", f"GIF 包含 {total_frames} 帧,仅会处理前 {self.rows * self.cols} 帧。" |
24 |
| - ) |
| 19 | + # 解码所有帧 |
| 20 | + for frame_idx in range(gif.n_frames): |
| 21 | + gif.seek(frame_idx) |
| 22 | + frames.append(gif.copy()) |
25 | 23 |
|
26 |
| - # 计算Sprite Sheet画布大小 |
27 |
| - sheet_width = self.cols * self.cell_width |
28 |
| - sheet_height = self.rows * self.cell_height |
29 |
| - spritesheet = Image.new("RGBA", (sheet_width, sheet_height), (255, 255, 255, 0)) |
| 24 | + total_frames = len(frames) |
| 25 | + if total_frames > self.rows * self.cols: |
| 26 | + messagebox.showwarning("帧数超限", f"GIF 包含 {total_frames} 帧,仅处理前 {self.rows * self.cols} 帧。") |
30 | 27 |
|
| 28 | + # 创建 Sprite Sheet |
| 29 | + spritesheet = Image.new("RGBA", (self.cols * self.cell_width, self.rows * self.cell_height), (255, 255, 255, 0)) |
31 | 30 | for idx, frame in enumerate(frames[:self.rows * self.cols]):
|
32 |
| - # 缩放帧到单元格大小(保持比例) |
33 | 31 | frame.thumbnail((self.cell_width, self.cell_height), Image.Resampling.LANCZOS)
|
34 |
| - |
35 |
| - # 计算居中偏移量 |
36 |
| - x_offset = (self.cell_width - frame.width) // 2 |
37 |
| - y_offset = (self.cell_height - frame.height) // 2 |
38 |
| - |
39 |
| - # 计算单元格位置 |
40 |
| - row = idx // self.cols |
41 |
| - col = idx % self.cols |
42 |
| - x = col * self.cell_width + x_offset |
43 |
| - y = row * self.cell_height + y_offset |
44 |
| - |
45 |
| - # 将帧粘贴到画布 |
| 32 | + x = (idx % self.cols) * self.cell_width + (self.cell_width - frame.width) // 2 |
| 33 | + y = (idx // self.cols) * self.cell_height + (self.cell_height - frame.height) // 2 |
46 | 34 | spritesheet.paste(frame, (x, y))
|
47 | 35 |
|
48 | 36 | spritesheet.save(output_file)
|
49 |
| - print(f"Sprite Sheet 已保存为 {output_file}") |
50 | 37 | except Exception as e:
|
51 |
| - raise RuntimeError(f"生成Sprite Sheet时发生错误: {e}") |
52 |
| - |
| 38 | + raise RuntimeError(f"生成 Sprite Sheet 错误: {e}") |
53 | 39 |
|
54 | 40 | class SpriteSheetApp:
|
55 | 41 | def __init__(self, root):
|
56 | 42 | self.root = root
|
57 | 43 | self.root.title("VRChar Dynamic Emoji Generator")
|
| 44 | + self.root.iconphoto(False, self.load_icon('icon.ico')) |
58 | 45 |
|
59 |
| - # 设置应用图标,使用资源路径获取 |
60 |
| - icon_path = self.resource_path('icon.ico') |
61 |
| - try: |
62 |
| - img = Image.open(icon_path) |
63 |
| - self.root.iconphoto(False, ImageTk.PhotoImage(img)) # 适用于跨平台 |
64 |
| - except Exception as e: |
65 |
| - print(f"图标加载失败: {e}") # 打印错误以便调试 |
| 46 | + # 固定窗口大小 |
| 47 | + self.root.geometry("400x300") |
| 48 | + self.root.resizable(False, False) |
66 | 49 |
|
67 | 50 | self.generator = SpriteSheetGenerator()
|
68 |
| - self.gif_path = None |
69 |
| - |
70 |
| - # 应用 ttk 自定义样式 |
71 |
| - self.apply_theme() |
72 |
| - |
73 |
| - # 界面布局 |
74 |
| - ttk.Label(root, text="选择一个 GIF 文件:").pack(pady=10) |
75 |
| - ttk.Label(root, text="Github: mmyo456/VRChar Dynamic Emoji Generator").pack(pady=10) |
76 |
| - |
77 |
| - self.select_button = ttk.Button(root, text="选择文件", command=self.select_file) |
78 |
| - self.select_button.pack(pady=5) |
| 51 | + self.setup_theme() |
| 52 | + self.setup_ui() |
79 | 53 |
|
80 |
| - frame = ttk.Frame(root) |
81 |
| - frame.pack(pady=10) |
82 |
| - |
83 |
| - ttk.Label(frame, text="单元格宽度:").grid(row=0, column=0, padx=5) |
84 |
| - self.cell_width_var = tk.IntVar(value=128) |
85 |
| - ttk.Entry(frame, textvariable=self.cell_width_var, width=10).grid(row=0, column=1, padx=5) |
86 |
| - |
87 |
| - ttk.Label(frame, text="单元格高度:").grid(row=1, column=0, padx=5) |
88 |
| - self.cell_height_var = tk.IntVar(value=128) |
89 |
| - ttk.Entry(frame, textvariable=self.cell_height_var, width=10).grid(row=1, column=1, padx=5) |
90 |
| - |
91 |
| - ttk.Label(frame, text="行数:").grid(row=0, column=2, padx=5) |
92 |
| - self.rows_var = tk.IntVar(value=8) |
93 |
| - ttk.Entry(frame, textvariable=self.rows_var, width=10).grid(row=0, column=3, padx=5) |
94 |
| - |
95 |
| - ttk.Label(frame, text="列数:").grid(row=1, column=2, padx=5) |
96 |
| - self.cols_var = tk.IntVar(value=8) |
97 |
| - ttk.Entry(frame, textvariable=self.cols_var, width=10).grid(row=1, column=3, padx=5) |
98 |
| - |
99 |
| - self.generate_button = ttk.Button(root, text="生成 Sprite Sheet", command=self.generate_spritesheet, state=tk.DISABLED) |
100 |
| - self.generate_button.pack(pady=10) |
101 |
| - |
102 |
| - def resource_path(self, relative_path): |
103 |
| - """获取打包后应用的资源文件路径""" |
| 54 | + def load_icon(self, icon_path): |
104 | 55 | try:
|
105 |
| - # 如果是打包成 .exe 文件 |
106 |
| - if getattr(sys, 'frozen', False): |
107 |
| - base_path = sys._MEIPASS |
108 |
| - else: |
109 |
| - base_path = os.path.abspath(".") |
110 |
| - return os.path.join(base_path, relative_path) |
111 |
| - except Exception as e: |
112 |
| - print(f"获取资源路径时出错: {e}") |
113 |
| - return relative_path |
| 56 | + path = os.path.join(getattr(sys, '_MEIPASS', os.path.abspath(".")), icon_path) |
| 57 | + return ImageTk.PhotoImage(Image.open(path)) |
| 58 | + except: |
| 59 | + return None |
114 | 60 |
|
115 |
| - def apply_theme(self): |
116 |
| - """应用自定义的暗色 ttk 样式""" |
| 61 | + def setup_theme(self): |
117 | 62 | style = ttk.Style(self.root)
|
118 | 63 | self.root.tk_setPalette(background="#2b2b2b", foreground="#ffffff")
|
| 64 | + style.theme_use("clam") |
| 65 | + style.configure("TButton", background="#444", foreground="#fff", font=("Arial", 10)) |
| 66 | + style.map("TButton", background=[("active", "#666")]) |
| 67 | + style.configure("TLabel", background="#2b2b2b", foreground="#fff") |
| 68 | + |
| 69 | + def setup_ui(self): |
| 70 | + ttk.Label(self.root, text="选择一个 GIF 文件:").pack(pady=5) |
| 71 | + ttk.Label(root, text="Github: mmyo456/VRChar Dynamic Emoji Generator").pack(pady=10) |
| 72 | + ttk.Button(self.root, text="选择文件", command=self.select_file).pack(pady=5) |
119 | 73 |
|
120 |
| - style.theme_use("clam") # 使用 ttk 的基础主题 |
121 |
| - style.configure( |
122 |
| - "TButton", |
123 |
| - background="#444444", |
124 |
| - foreground="#ffffff", |
125 |
| - font=("Arial", 10), |
126 |
| - borderwidth=1, |
127 |
| - focuscolor="#555555", |
128 |
| - ) |
129 |
| - style.map( |
130 |
| - "TButton", |
131 |
| - background=[("active", "#666666")], |
132 |
| - foreground=[("disabled", "#888888")], |
133 |
| - ) |
134 |
| - style.configure("TLabel", background="#2b2b2b", foreground="#ffffff", font=("Arial", 10)) |
| 74 | + # 参数输入框 |
| 75 | + self.vars = {k: tk.IntVar(value=v) for k, v in {"宽度": 128, "高度": 128, "行数": 8, "列数": 8}.items()} |
| 76 | + frame = ttk.Frame(self.root) |
| 77 | + frame.pack(pady=10) |
| 78 | + for idx, (label, var) in enumerate(self.vars.items()): |
| 79 | + ttk.Label(frame, text=label).grid(row=idx//2, column=2*(idx%2)) |
| 80 | + ttk.Entry(frame, textvariable=var, width=10).grid(row=idx//2, column=2*(idx%2)+1) |
| 81 | + |
| 82 | + self.generate_btn = ttk.Button(self.root, text="生成 Sprite Sheet", command=self.generate_spritesheet, state=tk.DISABLED) |
| 83 | + self.generate_btn.pack(pady=10) |
135 | 84 |
|
136 | 85 | def select_file(self):
|
137 | 86 | self.gif_path = filedialog.askopenfilename(filetypes=[("GIF files", "*.gif")])
|
138 | 87 | if self.gif_path:
|
139 |
| - self.generate_button.config(state=tk.NORMAL) |
| 88 | + self.generate_btn.config(state=tk.NORMAL) |
140 | 89 | messagebox.showinfo("文件已选择", f"已选择文件: {self.gif_path}")
|
141 | 90 |
|
142 | 91 | def generate_spritesheet(self):
|
143 |
| - if not self.gif_path: |
144 |
| - messagebox.showwarning("输入错误", "请选择一个有效的文件!") |
145 |
| - return |
146 |
| - |
147 | 92 | output_file = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG files", "*.png")])
|
148 |
| - if not output_file: |
149 |
| - return |
| 93 | + if not output_file: return |
150 | 94 |
|
151 | 95 | try:
|
152 |
| - self.generator.cell_width = self.cell_width_var.get() |
153 |
| - self.generator.cell_height = self.cell_height_var.get() |
154 |
| - self.generator.rows = self.rows_var.get() |
155 |
| - self.generator.cols = self.cols_var.get() |
156 |
| - |
157 |
| - self.generator.generate_spritesheet(self.gif_path, output_file) |
| 96 | + # 更新参数 |
| 97 | + self.generator.cell_width = self.vars["宽度"].get() |
| 98 | + self.generator.cell_height = self.vars["高度"].get() |
| 99 | + self.generator.rows = self.vars["行数"].get() |
| 100 | + self.generator.cols = self.vars["列数"].get() |
| 101 | + self.generator.generate(self.gif_path, output_file) |
158 | 102 | messagebox.showinfo("成功", f"Sprite Sheet 已保存到: {output_file}")
|
159 | 103 | except Exception as e:
|
160 |
| - messagebox.showerror("错误", f"生成 Sprite Sheet 时出错: {e}") |
| 104 | + messagebox.showerror("错误", str(e)) |
161 | 105 |
|
162 | 106 | if __name__ == "__main__":
|
163 | 107 | root = tk.Tk()
|
|
0 commit comments