ZHBPythonTool/jpg_to_png_gui.py
张宏博 450edbc9bb init
2025-12-19 16:20:53 +08:00

448 lines
17 KiB
Python
Raw Permalink 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 -*-
"""
JPG转PNG转换器 - GUI版本
使用tkinter创建图形界面支持文件和文件夹选择
"""
import os
import sys
import threading
from pathlib import Path
from tkinter import *
from tkinter import ttk, filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
from PIL import Image
import datetime
class JPGtoPNGConverterGUI:
def __init__(self, root):
self.root = root
self.root.title("JPG转PNG转换器 v1.0")
self.root.geometry("800x600")
self.root.resizable(True, True)
# 设置窗口图标(如果需要)
try:
# 可以设置窗口图标
pass
except:
pass
# 变量
self.input_path = StringVar()
self.output_path = StringVar()
self.conversion_mode = StringVar(value="folder") # "file" 或 "folder"
self.delete_original = BooleanVar(value=False)
self.recursive = BooleanVar(value=True)
self.is_converting = False
# 统计变量
self.total_files = 0
self.converted_files = 0
self.failed_files = 0
self.create_widgets()
def create_widgets(self):
"""创建界面组件"""
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(W, E, N, S))
# 配置网格权重
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# 标题
title_label = ttk.Label(main_frame, text="🎨 JPG转PNG转换器", font=("Arial", 16, "bold"))
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
# 转换模式选择
mode_frame = ttk.LabelFrame(main_frame, text="转换模式", padding="10")
mode_frame.grid(row=1, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
mode_frame.columnconfigure(0, weight=1)
ttk.Radiobutton(mode_frame, text="📁 转换文件夹(批量)",
variable=self.conversion_mode, value="folder",
command=self.on_mode_change).grid(row=0, column=0, sticky=W)
ttk.Radiobutton(mode_frame, text="📄 转换单个文件",
variable=self.conversion_mode, value="file",
command=self.on_mode_change).grid(row=0, column=1, sticky=W)
# 输入路径选择
input_frame = ttk.LabelFrame(main_frame, text="输入路径", padding="10")
input_frame.grid(row=2, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
input_frame.columnconfigure(1, weight=1)
ttk.Label(input_frame, text="输入:").grid(row=0, column=0, sticky=W, padx=(0, 5))
self.input_entry = ttk.Entry(input_frame, textvariable=self.input_path, width=50)
self.input_entry.grid(row=0, column=1, sticky=(W, E), padx=(0, 5))
self.input_browse_btn = ttk.Button(input_frame, text="浏览...", command=self.browse_input)
self.input_browse_btn.grid(row=0, column=2)
# 输出路径选择
output_frame = ttk.LabelFrame(main_frame, text="输出路径(可选)", padding="10")
output_frame.grid(row=3, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
output_frame.columnconfigure(1, weight=1)
ttk.Label(output_frame, text="输出:").grid(row=0, column=0, sticky=W, padx=(0, 5))
ttk.Entry(output_frame, textvariable=self.output_path, width=50).grid(row=0, column=1, sticky=(W, E), padx=(0, 5))
self.output_browse_btn = ttk.Button(output_frame, text="浏览...", command=self.browse_output)
self.output_browse_btn.grid(row=0, column=2)
ttk.Label(output_frame, text="💡 留空将在原位置生成PNG文件",
foreground="gray").grid(row=1, column=0, columnspan=3, sticky=W, pady=(5, 0))
# 选项设置
options_frame = ttk.LabelFrame(main_frame, text="转换选项", padding="10")
options_frame.grid(row=4, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
ttk.Checkbutton(options_frame, text="🗑️ 转换后删除原JPG文件",
variable=self.delete_original).grid(row=0, column=0, sticky=W)
self.recursive_check = ttk.Checkbutton(options_frame, text="📁 递归处理子文件夹",
variable=self.recursive)
self.recursive_check.grid(row=1, column=0, sticky=W)
# 控制按钮
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=5, column=0, columnspan=3, pady=20)
self.start_btn = ttk.Button(button_frame, text="🚀 开始转换",
command=self.start_conversion, style="Accent.TButton")
self.start_btn.pack(side=LEFT, padx=(0, 10))
self.stop_btn = ttk.Button(button_frame, text="⏹️ 停止",
command=self.stop_conversion, state=DISABLED)
self.stop_btn.pack(side=LEFT, padx=(0, 10))
ttk.Button(button_frame, text="🗂️ 打开输出文件夹",
command=self.open_output_folder).pack(side=LEFT)
# 进度条
progress_frame = ttk.Frame(main_frame)
progress_frame.grid(row=6, column=0, columnspan=3, sticky=(W, E), pady=(0, 10))
progress_frame.columnconfigure(0, weight=1)
self.progress_var = DoubleVar()
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var,
maximum=100, mode='determinate')
self.progress_bar.grid(row=0, column=0, sticky=(W, E), padx=(0, 5))
self.progress_label = ttk.Label(progress_frame, text="准备就绪")
self.progress_label.grid(row=0, column=1)
# 状态信息显示
status_frame = ttk.LabelFrame(main_frame, text="转换日志", padding="5")
status_frame.grid(row=7, column=0, columnspan=3, sticky=(W, E, N, S), pady=(0, 10))
status_frame.columnconfigure(0, weight=1)
status_frame.rowconfigure(0, weight=1)
self.log_text = ScrolledText(status_frame, height=12, wrap=WORD)
self.log_text.grid(row=0, column=0, sticky=(W, E, N, S))
# 配置主框架的行权重
main_frame.rowconfigure(7, weight=1)
# 初始化界面状态
self.on_mode_change()
def on_mode_change(self):
"""转换模式改变时的处理"""
if self.conversion_mode.get() == "file":
self.recursive_check.configure(state=DISABLED)
self.input_browse_btn.configure(text="选择文件...")
self.output_browse_btn.configure(text="保存为...")
else:
self.recursive_check.configure(state=NORMAL)
self.input_browse_btn.configure(text="选择文件夹...")
self.output_browse_btn.configure(text="选择文件夹...")
def browse_input(self):
"""浏览输入路径"""
if self.conversion_mode.get() == "file":
# 选择文件
file_path = filedialog.askopenfilename(
title="选择JPG文件",
filetypes=[
("JPG文件", "*.jpg *.jpeg *.JPG *.JPEG"),
("所有文件", "*.*")
]
)
if file_path:
self.input_path.set(file_path)
else:
# 选择文件夹
folder_path = filedialog.askdirectory(title="选择包含JPG文件的文件夹")
if folder_path:
self.input_path.set(folder_path)
def browse_output(self):
"""浏览输出路径"""
if self.conversion_mode.get() == "file":
# 保存文件
file_path = filedialog.asksaveasfilename(
title="保存PNG文件为",
defaultextension=".png",
filetypes=[
("PNG文件", "*.png"),
("所有文件", "*.*")
]
)
if file_path:
self.output_path.set(file_path)
else:
# 选择输出文件夹
folder_path = filedialog.askdirectory(title="选择输出文件夹")
if folder_path:
self.output_path.set(folder_path)
def log_message(self, message):
"""添加日志信息"""
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
self.log_text.insert(END, f"[{timestamp}] {message}\n")
self.log_text.see(END)
self.root.update_idletasks()
def update_progress(self, current, total, status=""):
"""更新进度条"""
if total > 0:
progress = (current / total) * 100
self.progress_var.set(progress)
if status:
self.progress_label.configure(text=status)
else:
self.progress_label.configure(text=f"{current}/{total}")
self.root.update_idletasks()
def convert_single_file(self, input_file, output_file=None):
"""转换单个文件"""
try:
input_path = Path(input_file)
if not input_path.exists():
self.log_message(f"❌ 文件不存在: {input_file}")
return False
# 设置输出路径
if output_file:
output_path = Path(output_file)
else:
output_path = input_path.with_suffix('.png')
# 确保输出目录存在
output_path.parent.mkdir(parents=True, exist_ok=True)
self.log_message(f"🔄 转换: {input_path.name} -> {output_path.name}")
# 转换图片
with Image.open(input_path) as img:
if img.mode in ('RGBA', 'LA'):
img.save(output_path, 'PNG', optimize=True)
else:
if img.mode == 'P':
img = img.convert('RGBA')
elif img.mode in ('RGB', 'L'):
img = img.convert('RGBA')
img.save(output_path, 'PNG', optimize=True)
self.log_message(f"✅ 转换成功: {output_path}")
self.converted_files += 1
# 删除原文件(如果选择)
if self.delete_original.get():
try:
input_path.unlink()
self.log_message(f"🗑️ 已删除原文件: {input_path}")
except Exception as e:
self.log_message(f"⚠️ 删除原文件失败: {e}")
return True
except Exception as e:
self.log_message(f"❌ 转换失败 {input_file}: {e}")
self.failed_files += 1
return False
def find_jpg_files(self, folder_path, recursive=True):
"""查找JPG文件"""
folder = Path(folder_path)
jpg_files = []
patterns = ['*.jpg', '*.jpeg', '*.JPG', '*.JPEG']
if recursive:
for pattern in patterns:
jpg_files.extend(folder.rglob(pattern))
else:
for pattern in patterns:
jpg_files.extend(folder.glob(pattern))
return jpg_files
def conversion_worker(self):
"""转换工作线程"""
try:
self.total_files = 0
self.converted_files = 0
self.failed_files = 0
input_path = self.input_path.get().strip()
output_path = self.output_path.get().strip()
if not input_path:
self.log_message("❌ 请选择输入路径")
return
if self.conversion_mode.get() == "file":
# 单文件转换
self.total_files = 1
self.update_progress(0, 1, "转换中...")
output_file = output_path if output_path else None
self.convert_single_file(input_path, output_file)
self.update_progress(1, 1, "完成")
else:
# 批量转换文件夹
self.log_message(f"📁 扫描文件夹: {input_path}")
jpg_files = self.find_jpg_files(input_path, self.recursive.get())
if not jpg_files:
self.log_message("📷 未找到JPG文件")
return
self.total_files = len(jpg_files)
self.log_message(f"📷 找到 {self.total_files} 个JPG文件")
# 转换每个文件
for i, jpg_file in enumerate(jpg_files):
if not self.is_converting: # 检查是否被停止
break
# 计算输出路径
if output_path:
relative_path = jpg_file.relative_to(Path(input_path))
output_file = Path(output_path) / relative_path.with_suffix('.png')
else:
output_file = None
self.update_progress(i, self.total_files, f"转换中 {i+1}/{self.total_files}")
self.convert_single_file(jpg_file, output_file)
self.update_progress(self.total_files, self.total_files, "完成")
# 显示转换摘要
self.log_message("=" * 50)
self.log_message(f"🎯 转换摘要")
self.log_message(f"📊 总文件数: {self.total_files}")
self.log_message(f"✅ 成功转换: {self.converted_files}")
self.log_message(f"❌ 转换失败: {self.failed_files}")
self.log_message("=" * 50)
if self.converted_files > 0:
messagebox.showinfo("转换完成",
f"转换完成!\n"
f"成功: {self.converted_files} 个文件\n"
f"失败: {self.failed_files} 个文件")
except Exception as e:
self.log_message(f"❌ 转换过程出错: {e}")
messagebox.showerror("错误", f"转换过程出错: {e}")
finally:
self.is_converting = False
self.start_btn.configure(state=NORMAL)
self.stop_btn.configure(state=DISABLED)
def start_conversion(self):
"""开始转换"""
if self.is_converting:
return
# 验证输入
if not self.input_path.get().strip():
messagebox.showerror("错误", "请选择输入路径")
return
if not Path(self.input_path.get()).exists():
messagebox.showerror("错误", "输入路径不存在")
return
# 清空日志
self.log_text.delete(1.0, END)
# 开始转换
self.is_converting = True
self.start_btn.configure(state=DISABLED)
self.stop_btn.configure(state=NORMAL)
# 在新线程中执行转换
threading.Thread(target=self.conversion_worker, daemon=True).start()
def stop_conversion(self):
"""停止转换"""
self.is_converting = False
self.log_message("⏹️ 用户停止转换")
self.start_btn.configure(state=NORMAL)
self.stop_btn.configure(state=DISABLED)
def open_output_folder(self):
"""打开输出文件夹"""
output_path = self.output_path.get().strip()
if output_path and Path(output_path).exists():
os.startfile(output_path)
elif self.input_path.get().strip():
input_path = self.input_path.get().strip()
if Path(input_path).exists():
if Path(input_path).is_file():
os.startfile(Path(input_path).parent)
else:
os.startfile(input_path)
else:
messagebox.showwarning("警告", "没有有效的输出路径")
def main():
"""主函数"""
root = Tk()
# 设置样式
style = ttk.Style()
# 尝试使用现代主题
try:
style.theme_use('winnative') # Windows原生样式
except:
pass
# 创建应用
app = JPGtoPNGConverterGUI(root)
# 设置窗口关闭事件
def on_closing():
if app.is_converting:
if messagebox.askokcancel("退出", "正在转换中,确定要退出吗?"):
app.is_converting = False
root.destroy()
else:
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
# 运行主循环
root.mainloop()
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"程序启动失败: {e}")
import traceback
traceback.print_exc()