import tkinter as tkfrom tkinter import ttk, filedialog, messageboxfrom tkinter.font import Fontclass ImageUnzipTool: def __init__(self, root): self.root = root self.root.title("zip/word/excel多文档图片批量提取工具(欢迎关注微信公众号:码海听潮)") self.root.geometry("760x615") self.root.resizable(False, False) # 设置主题风格 self.style = ttk.Style() self.style.theme_use('clam') # 自定义字体 self.title_font = Font(family="微软雅黑", size=16, weight="bold") self.label_font = Font(family="微软雅黑", size=10) self.button_font = Font(family="微软雅黑", size=10, weight="bold") # 初始化变量 self.input_path = tk.StringVar() self.output_dir = tk.StringVar() self.quality = tk.StringVar(value="95") # 默认开启递归处理,保持目录结构 self.is_recursive = True self.preserve_structure = True self.selected_files = [] self.total_files = 0 self.processed_files = 0 self.is_processing = False self.temp_zip_paths = [] self.supported_extensions = ('.zip', '.xlsx', '.xls', '.docx', '.doc') # 创建UI self.create_widgets() # 应用配色方案 self.apply_styles() def apply_styles(self): # 主色调 primary_color = "#4A7ABC" secondary_color = "#E0E7FF" accent_color = "#6366F1" neutral_color = "#F5F7FA" # 配置样式 self.style.configure('TFrame', background=neutral_color) self.style.configure('TLabel', background=neutral_color, font=self.label_font) self.style.configure('Title.TLabel', font=self.title_font, background=primary_color, foreground='white') self.style.configure('TButton', font=self.button_font, background=primary_color, foreground='white') self.style.configure('Accent.TButton', font=self.button_font, background=accent_color, foreground='white') self.style.configure('TProgressbar', background=accent_color) self.style.configure('Header.TFrame', background=primary_color) self.style.configure('FileList.TFrame', background=secondary_color) self.style.configure('Control.TFrame', background=secondary_color) def create_widgets(self): # 主内容区域 main_frame = ttk.Frame(self.root, padding="20") main_frame.pack(expand=True, fill=tk.BOTH) # 输入文件夹选择区域 input_frame = ttk.LabelFrame(main_frame, text="选择要处理的文件夹", padding=10) input_frame.pack(fill=tk.X, pady=5) # 文件夹路径显示 path_frame = ttk.Frame(input_frame) path_frame.pack(fill=tk.X) path_entry = ttk.Entry(path_frame, textvariable=self.input_path, width=50) path_entry.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X) # 为文件夹路径输入框添加工具提示 ToolTip(path_entry, "选择包含ZIP、Excel或Word文件的文件夹,支持递归搜索子文件夹") select_folder_btn = ttk.Button( path_frame, text="选择文件夹", command=self.select_folder, style='Accent.TButton', width=15 ) select_folder_btn.pack(side=tk.RIGHT, padx=5) # 输出目录选择 output_frame = ttk.LabelFrame(main_frame, text="输出目录", padding=10) output_frame.pack(fill=tk.X, pady=5) output_entry = ttk.Entry(output_frame, textvariable=self.output_dir, width=50) output_entry.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X) # 为输出目录输入框添加工具提示 ToolTip(output_entry, "设置提取图片的保存位置,如不设置,将自动在源文件夹同级创建extracted_images文件夹") output_btn = ttk.Button(output_frame, text="选择目录", command=self.select_output_dir) output_btn.pack(side=tk.RIGHT, padx=5) # 图片质量设置 quality_frame = ttk.LabelFrame(main_frame, text="图片质量设置", padding=10) quality_frame.pack(fill=tk.X, pady=5) quality_control_frame = ttk.Frame(quality_frame) quality_control_frame.pack(fill=tk.X) quality_label = ttk.Label(quality_control_frame, text="保存质量 (1-100):") quality_label.pack(side=tk.LEFT, padx=5) self.quality_entry = ttk.Entry(quality_control_frame, textvariable=self.quality, width=20, font=('微软雅黑', 10)) self.quality_entry.pack(side=tk.LEFT, padx=10) # 为质量输入框添加工具提示 ToolTip(self.quality_entry, "低质量(60)-中等质量(80)\n高质量(95)-无损(100)") # 验证输入的质量值 def validate_quality(*args): try: value = self.quality.get().strip() if value: quality_value = int(value) if quality_value < 1 or quality_value > 100: self.quality.set("95") else: self.quality.set("95") except ValueError: self.quality.set("95") # 绑定验证事件 self.quality.trace_add("write", validate_quality) # 文件列表区域 - 可以滚动和调整大小 list_frame = ttk.LabelFrame(main_frame, text="发现的文件及图片预览", padding=10) list_frame.pack(fill=tk.BOTH, expand=True, pady=5) # 改为 expand=True list_frame.pack_propagate(False) # 禁止自动调整大小 list_frame.configure(height=250) # 设置最小高度 # 创建Canvas和滚动条 canvas = tk.Canvas(list_frame, bg="#F0F0F0", highlightthickness=0) scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview) self.file_list_frame = ttk.Frame(canvas, style='FileList.TFrame') # 设置Canvas滚动区域 self.file_list_frame.bind( "<Configure>", lambda e: canvas.configure( scrollregion=canvas.bbox("all") ) ) # 鼠标滚轮支持 def on_mousewheel(event): canvas.yview_scroll(int(-1*(event.delta/120)), "units") canvas.bind_all("<MouseWheel>", on_mousewheel) canvas.create_window((0, 0), window=self.file_list_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # 开始批量提取按钮 - 直接在主框架中居中 self.process_btn = ttk.Button( main_frame, text="🚀 开始批量提取", command=self.process_files, style='Accent.TButton', width=20 ) self.process_btn.pack(pady=10, ipadx=20) # 为处理按钮添加工具提示 ToolTip(self.process_btn, "开始批量提取所有找到的图片文件\n将会保持原有的文件夹结构") # 统计信息 stats_frame = ttk.Frame(main_frame) stats_frame.pack(fill=tk.X, pady=5) self.stats_var = tk.StringVar(value="") stats_label = ttk.Label(stats_frame, textvariable=self.stats_var, foreground="#666666") stats_label.pack(side=tk.RIGHT) def select_folder(self): """选择文件夹""" self.cleanup_temp_files() folder_path = filedialog.askdirectory(title="选择包含文件的文件夹") if folder_path: self.input_path.set(folder_path) self.load_files_from_folder(folder_path) # 自动设置输出目录(在输入文件夹同级创建输出文件夹) if not self.output_dir.get(): default_output = os.path.join(os.path.dirname(folder_path), "extracted_images") self.output_dir.set(default_output) def load_files_from_folder(self, folder_path): """从文件夹加载所有支持的文件""" self.clear_file_list() self.selected_files = [] files = [] try: # 递归遍历文件夹(默认开启) for root, dirs, filenames in os.walk(folder_path): for filename in filenames: if filename.lower().endswith(self.supported_extensions): full_path = os.path.join(root, filename) files.append(full_path) if files: self.load_files(files, folder_path) self.update_stats() else: ttk.Label(self.file_list_frame, text="未找到支持的文件格式 (.zip, .xlsx, .xls, .docx, .doc)").pack(pady=10) self.stats_var.set("") except Exception as e: messagebox.showerror("错误", f"加载文件夹失败: {str(e)}") def load_files(self, file_paths, base_folder=None): """加载文件列表并预览内容""" self.clear_file_list() self.selected_files = [] for file_path in file_paths: if not os.path.exists(file_path): continue file_ext = os.path.splitext(file_path)[1].lower() relative_path = "" if base_folder: relative_path = os.path.relpath(file_path, base_folder) try: # 确定要使用的ZIP路径 if file_ext in ['.xlsx', '.docx']: # 创建临时ZIP文件 temp_dir = tempfile.mkdtemp() temp_zip_path = os.path.join(temp_dir, f"temp_{os.getpid()}_{len(self.temp_zip_paths)}.zip") shutil.copy2(file_path, temp_zip_path) self.temp_zip_paths.append(temp_zip_path) zip_to_check = temp_zip_path elif file_ext == '.zip': zip_to_check = file_path else: continue # 检查ZIP中的图片文件 with ZipFile(zip_to_check) as zf: image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff') image_files = [item for item in zf.filelist if item.filename.lower().endswith(image_extensions)] if image_files: # 创建文件组显示 self.create_file_group_display(file_path, relative_path, image_files) self.selected_files.append({ 'original_path': file_path, 'zip_path': zip_to_check, 'images': image_files, 'relative_path': relative_path }) except Exception as e: print(f"警告: 无法读取文件 {file_path}: {str(e)}") continue def create_file_group_display(self, file_path, relative_path, image_files): """创建文件组显示""" # 文件组容器 group_frame = ttk.Frame(self.file_list_frame, style='FileList.TFrame', relief='solid', borderwidth=1) group_frame.pack(fill=tk.X, pady=3, padx=5) # 文件头部信息 header_frame = ttk.Frame(group_frame) header_frame.pack(fill=tk.X, padx=10, pady=5) # 文件类型图标 file_ext = os.path.splitext(file_path)[1].lower() if file_ext == '.zip': icon = "📦" elif file_ext in ['.xlsx', '.xls']: icon = "📊" elif file_ext in ['.docx', '.doc']: icon = "📝" else: icon = "📄" icon_label = ttk.Label(header_frame, text=icon, font=('微软雅黑', 12)) icon_label.pack(side=tk.LEFT, padx=5) # 文件信息 info_frame = ttk.Frame(header_frame) info_frame.pack(side=tk.LEFT, fill=tk.X, expand=True) file_name = os.path.basename(file_path) name_label = ttk.Label(info_frame, text=file_name, font=('微软雅黑', 10, 'bold')) name_label.pack(anchor=tk.W) if relative_path: path_label = ttk.Label(info_frame, text=f"📂 {relative_path}", font=('微软雅黑', 8), foreground="#666666") path_label.pack(anchor=tk.W) # 图片数量统计 stats_frame = ttk.Frame(header_frame) stats_frame.pack(side=tk.RIGHT, padx=5) count_label = ttk.Label(stats_frame, text=f"🖼️ {len(image_files)} 张图片", font=('微软雅黑', 9, 'bold'), foreground="#4A7ABC") count_label.pack() # 预览图片列表(折叠显示) if len(image_files) > 0: preview_frame = ttk.Frame(group_frame) preview_frame.pack(fill=tk.X, padx=20, pady=(0, 5)) # 显示前3张图片 for i, img in enumerate(image_files[:3]): img_name = os.path.basename(img.filename) img_label = ttk.Label(preview_frame, text=f"├─ {img_name}", font=('微软雅黑', 8)) img_label.pack(anchor=tk.W) if len(image_files) > 3: more_label = ttk.Label(preview_frame, text=f"└─ ... 还有 {len(image_files) - 3} 张图片", font=('微软雅黑', 8), foreground="#666666") more_label.pack(anchor=tk.W) def update_stats(self): """更新统计信息""" total_files = len(self.selected_files) total_images = sum(len(item['images']) for item in self.selected_files) # 统计文件类型 file_types = {} for item in self.selected_files: ext = os.path.splitext(item['original_path'])[1].lower() if ext in file_types: file_types[ext] += 1 else: file_types[ext] = 1 stats_text = f"统计: {total_files} 个文件 | {total_images} 张图片 " for ext, count in file_types.items(): stats_text += f"| {ext}: {count} " self.stats_var.set(stats_text) def clear_file_list(self): """清空文件列表""" for widget in self.file_list_frame.winfo_children(): widget.destroy() def select_output_dir(self): """选择输出目录""" dir_path = filedialog.askdirectory(title="选择输出目录") if dir_path: self.output_dir.set(dir_path) def get_quality_value(self): """获取有效的质量值""" try: value = self.quality.get().strip() if value: quality = int(value) if 1 <= quality <= 100: return quality except ValueError: pass return 95 def process_files(self): """开始处理文件""" if self.is_processing: return output_path = self.output_dir.get() if not self.selected_files: messagebox.showwarning("警告", "没有找到可处理的文件,请先选择文件夹") return if not output_path: # 如果没有设置输出目录,自动创建 input_folder = self.input_path.get() output_path = os.path.join(os.path.dirname(input_folder), "extracted_images") self.output_dir.set(output_path) # 验证质量值 quality_value = self.get_quality_value() self.quality.set(str(quality_value)) total_images = sum(len(item['images']) for item in self.selected_files) # 确认对话框 confirm_msg = f"""准备开始批量提取图片:📁 源文件夹: {self.input_path.get()}📂 输出目录: {output_path}📦 文件总数: {len(self.selected_files)}🖼️ 图片总数: {total_images}🔄 递归处理: 是📊 保持目录结构: 是🎨 图片质量: {quality_value}%是否继续?""" if messagebox.askyesno("确认批量处理", confirm_msg): self.is_processing = True self.process_btn.config(state=tk.DISABLED) self.processed_files = 0 thread = threading.Thread(target=self.process_files_thread) thread.daemon = True thread.start() def process_files_thread(self): """处理文件的线程""" try: quality = self.get_quality_value() output_path = self.output_dir.get() for file_item in self.selected_files: if not self.is_processing: break try: zip_path = file_item['zip_path'] images = file_item['images'] relative_path = file_item['relative_path'] source_filename = os.path.splitext(os.path.basename(file_item['original_path']))[0] # 保持原目录结构 if relative_path: # 获取文件所在目录的相对路径 base_dir = os.path.dirname(relative_path) file_output_dir = os.path.join(output_path, base_dir, source_filename) else: file_output_dir = os.path.join(output_path, source_filename) # 创建输出目录 os.makedirs(file_output_dir, exist_ok=True) with ZipFile(zip_path) as zf: for image_item in images: if not self.is_processing: break try: image_name = image_item.filename safe_name = os.path.basename(image_name) # 清理文件名 safe_name = "".join(c for c in safe_name if c.isalnum() or c in "._- ") output_file = os.path.join(file_output_dir, safe_name) # 如果文件已存在,添加数字后缀 output_file = self.get_unique_filename(output_file) # 读取并保存图片 img_data = zf.read(image_name) img = Image.open(BytesIO(img_data)) # 转换RGBA到RGB(如果需要) if img.mode == 'RGBA' and img.format and img.format.lower() == 'jpeg': img = img.convert('RGB') if img.format and img.format.lower() == 'jpeg': img.save(output_file, quality=quality, optimize=True) else: img.save(output_file) self.processed_files += 1 time.sleep(0.01) except Exception as e: print(f"处理图片 {image_name} 时出错: {str(e)}") continue except Exception as e: print(f"处理文件 {file_item['original_path']} 时出错: {str(e)}") continue if self.is_processing: # 处理完成 success_msg = f"""批量提取完成!📊 处理结果:✅ 成功提取: {self.processed_files} 张图片📁 源文件数: {len(self.selected_files)}📂 输出目录: {output_path}🎨 使用质量: {quality}%图片已按原目录结构保存。""" messagebox.showinfo("处理完成", success_msg) # 询问是否打开输出文件夹 if messagebox.askyesno("提示", "是否打开输出文件夹?"): os.startfile(output_path) except Exception as e: messagebox.showerror("错误", f"处理过程中出现错误: {str(e)}") finally: self.is_processing = False self.process_btn.config(state=tk.NORMAL) self.cleanup_temp_files() def get_unique_filename(self, filepath): """如果文件已存在,添加数字后缀""" if not os.path.exists(filepath): return filepath directory = os.path.dirname(filepath) filename = os.path.basename(filepath) name, ext = os.path.splitext(filename) counter = 1 while True: new_filename = f"{name}_{counter}{ext}" new_filepath = os.path.join(directory, new_filename) if not os.path.exists(new_filepath): return new_filepath counter += 1 def cleanup_temp_files(self): """清理所有临时文件""" for temp_path in self.temp_zip_paths: if temp_path and os.path.exists(temp_path): try: temp_dir = os.path.dirname(temp_path) shutil.rmtree(temp_dir) except Exception as e: print(f"警告: 清理临时文件失败: {str(e)}") self.temp_zip_paths = []if __name__ == "__main__": root = tk.Tk() app = ImageUnzipTool(root) # 设置窗口图标 try: root.iconbitmap("icon.ico") except: pass root.mainloop()