python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python批量图片缩放

Python基于Tkinter打造高颜值批量图片缩放工具(可直接落地)

作者:幸福清风

日常办公、自媒体运营、前端开发中,经常需要批量修改图片分辨率,本文将基于 Python 原生 GUI 库 Tkinter + 专业图像处理库 Pillow开发一款免费无水印、全格式兼容、带图形界面的批量图片缩放工具,,感兴趣的小伙伴可以了解下

一、项目背景

日常办公、自媒体运营、前端开发中,经常需要批量修改图片分辨率。市面上的图片处理工具大多存在有水印、批量收费、操作繁琐、格式兼容差等问题。

为此,我基于 Python 原生 GUI 库 Tkinter + 专业图像处理库 Pillow,开发了一款免费无水印、全格式兼容、带图形界面的批量图片缩放工具。工具支持自定义分辨率、保持宽高比、批量导入导出、文件去重、进度实时展示,完美解决日常图片批量缩放需求,零基础可直接运行使用。

二、技术栈介绍

本项目采用轻量化、零门槛技术组合,无需复杂环境配置,适配Windows、Mac、Linux全平台:

三、工具核心功能亮点

相比网上开源的简易图片缩放脚本,本工具做了大量实用性优化和BUG修复,核心功能如下:

1. 全格式批量处理

支持 JPG、JPEG、PNG、BMP、GIF、TIFF、WEBP、ICO 等几乎所有主流图片格式,一次性批量导入多张图片,批量完成缩放处理。

2. 智能分辨率适配

3. 人性化文件处理逻辑

4. 可视化交互体验

5. 全版本兼容适配

四、核心技术原理与关键代码解析

1. 高清缩放算法兼容处理

Pillow 9.1.0 以上版本将缩放常量迁移至 Image.Resampling,旧版本直接使用 Image.LANCZOS,通过异常捕获实现全版本兼容,避免运行报错:

try:
    # 新版PIL高清缩放
    RESAMPLE_FILTER = Image.Resampling.LANCZOS
except AttributeError:
    # 兼容旧版PIL
    RESAMPLE_FILTER = Image.LANCZOS

LANCZOS 算法能够实现高清无损缩放,相比默认算法,缩放后图片边缘更平滑,无锯齿模糊。

2. 宽高比智能适配核心逻辑

这是图片缩放的核心难点,精准规避拉伸变形,通过对比原图与目标画布的比例,自动适配最优尺寸:

if self.keep_aspect_ratio.get():
    # 计算原始比例与目标比例
    o_ratio = ow / oh
    t_ratio = target_w / target_h
    # 按比例自适应缩放,留白不拉伸
    if o_ratio > t_ratio:
        nw, nh = target_w, int(target_w / o_ratio)
    else:
        nh, nw = target_h, int(target_h * o_ratio)
else:
    # 强制自定义尺寸
    nw, nh = target_w, target_h

3. 输入框严格校验(防非法参数)

通过Tkinter校验机制,限制宽高输入框仅可输入数字,彻底杜绝字母、符号、空值、负数导致的程序崩溃:

def bind_num_only(self):
    """限制宽高输入框只能输入数字"""
    def only_num(input_str):
        return input_str.isdigit() or input_str == ""
    validate_cmd = self.root.register(only_num)
    self.width_entry.config(validate="key", validatecommand=(validate_cmd, "%P"))
    self.height_entry.config(validate="key", validatecommand=(validate_cmd, "%P"))

4. 文件防覆盖去重逻辑

自动检测输出目录同名文件,通过后缀递增实现文件不覆盖、不丢失:

cnt = 1
while os.path.exists(save_path):
    save_path = os.path.join(self.output_path, f"{base}_resized{cnt}{ext}")
    cnt += 1

5. 界面防卡顿优化

大批量图片处理时,通过 update_idletasks() 实时刷新UI,同步更新进度文本,解决程序假死问题:

self.progress_var.set(f"处理中 {idx+1}/{total}: {os.path.basename(img_path)}")
self.root.update_idletasks()

五、完整开源代码

环境依赖安装

pip install pillow

完整可运行源码:

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image
import os

# 兼容新旧版本Pillow缩放常量
try:
    RESAMPLE_FILTER = Image.Resampling.LANCZOS
except AttributeError:
    RESAMPLE_FILTER = Image.LANCZOS


class ImageResizerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("图片分辨率修改工具")
        self.root.geometry("800x650")
        self.root.configure(bg="#f0f0f0")
        # 窗口自适应权重配置
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

        # 全局变量初始化
        self.image_paths = []
        self.target_width = tk.StringVar(value="800")
        self.target_height = tk.StringVar(value="600")
        self.keep_aspect_ratio = tk.BooleanVar(value=True)
        self.output_path = ""

        self.setup_ui()
        self.bind_num_only()

    def bind_num_only(self):
        """限制宽高输入框仅输入数字"""

        def only_num(input_str):
            return input_str.isdigit() or input_str == ""

        validate_cmd = self.root.register(only_num)
        self.width_entry.config(validate="key", validatecommand=(validate_cmd, "%P"))
        self.height_entry.config(validate="key", validatecommand=(validate_cmd, "%P"))

    def setup_ui(self):
        """初始化UI界面"""
        # 标题
        title_label = tk.Label(
            self.root,
            text="图片分辨率修改工具",
            font=("Arial", 18, "bold"),
            bg="#f0f0f0",
            fg="#333"
        )
        title_label.pack(pady=15)

        # 主框架
        main_frame = tk.Frame(self.root, bg="#f0f0f0")
        main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(0, weight=1)

        # 左侧控制面板
        control_frame = tk.LabelFrame(
            main_frame, text="控制面板", font=("Arial", 12),
            bg="#f0f0f0", fg="#333", padx=10, pady=10
        )
        control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))

        # 分辨率设置模块
        resolution_frame = tk.LabelFrame(
            control_frame, text="分辨率设置", font=("Arial", 10),
            bg="#f0f0f0", fg="#333"
        )
        resolution_frame.pack(fill=tk.X, pady=(0, 15))

        # 宽度输入
        width_frame = tk.Frame(resolution_frame, bg="#f0f0f0")
        width_frame.pack(fill=tk.X, pady=5)
        tk.Label(width_frame, text="宽度:", font=("Arial", 10), bg="#f0f0f0").pack(side=tk.LEFT)
        self.width_entry = tk.Entry(
            width_frame, textvariable=self.target_width,
            font=("Arial", 10), width=10
        )
        self.width_entry.pack(side=tk.RIGHT)

        # 高度输入
        height_frame = tk.Frame(resolution_frame, bg="#f0f0f0")
        height_frame.pack(fill=tk.X, pady=5)
        tk.Label(height_frame, text="高度:", font=("Arial", 10), bg="#f0f0f0").pack(side=tk.LEFT)
        self.height_entry = tk.Entry(
            height_frame, textvariable=self.target_height,
            font=("Arial", 10), width=10
        )
        self.height_entry.pack(side=tk.RIGHT)

        # 比例锁定复选框
        aspect_check = tk.Checkbutton(
            resolution_frame, text="保持原始宽高比",
            variable=self.keep_aspect_ratio, font=("Arial", 10), bg="#f0f0f0"
        )
        aspect_check.pack(pady=5)

        # 导入导出模块
        io_frame = tk.LabelFrame(
            control_frame, text="导入导出设置", font=("Arial", 10),
            bg="#f0f0f0", fg="#333"
        )
        io_frame.pack(fill=tk.X, pady=10)

        # 导入路径展示
        tk.Label(io_frame, text="导入图片:", font=("Arial", 10), bg="#f0f0f0").pack(anchor=tk.W, pady=(0, 5))
        self.import_path_var = tk.StringVar(value="未选择")
        import_path_label = tk.Label(
            io_frame, textvariable=self.import_path_var, font=("Arial", 9),
            bg="#ffffff", relief=tk.SUNKEN, anchor=tk.W, wraplength=180
        )
        import_path_label.pack(fill=tk.X, pady=(0, 5))

        # 导入按钮
        import_btn = tk.Button(
            io_frame, text="导入图片", command=self.import_images,
            bg="#4CAF50", fg="white", font=("Arial", 10, "bold"),
            relief=tk.FLAT, padx=10, pady=5
        )
        import_btn.pack(fill=tk.X, pady=5)

        # 输出路径展示
        tk.Label(io_frame, text="输出路径:", font=("Arial", 10), bg="#f0f0f0").pack(anchor=tk.W, pady=(0, 5))
        self.output_path_var = tk.StringVar(value="未选择")
        output_path_label = tk.Label(
            io_frame, textvariable=self.output_path_var, font=("Arial", 9),
            bg="#ffffff", relief=tk.SUNKEN, anchor=tk.W, wraplength=180
        )
        output_path_label.pack(fill=tk.X, pady=(0, 5))

        # 选择输出路径按钮
        select_output_btn = tk.Button(
            io_frame, text="选择输出路径", command=self.select_output_path,
            bg="#9C27B0", fg="white", font=("Arial", 10, "bold"),
            relief=tk.FLAT, padx=10, pady=3
        )
        select_output_btn.pack(fill=tk.X)

        # 功能按钮组
        button_frame = tk.Frame(control_frame, bg="#f0f0f0")
        button_frame.pack(fill=tk.X, pady=10)

        start_btn = tk.Button(
            button_frame, text="开始处理", command=self.start_processing,
            bg="#2196F3", fg="white", font=("Arial", 10, "bold"),
            relief=tk.FLAT, padx=10, pady=5
        )
        start_btn.pack(fill=tk.X, pady=5)

        del_btn = tk.Button(
            button_frame, text="删除选中图片", command=self.delete_selected_img,
            bg="#ff6b6b", fg="white", font=("Arial", 10, "bold"),
            relief=tk.FLAT, padx=10, pady=5
        )
        del_btn.pack(fill=tk.X, pady=5)

        reset_btn = tk.Button(
            button_frame, text="重置全部", command=self.reset,
            bg="#FF9800", fg="white", font=("Arial", 10, "bold"),
            relief=tk.FLAT, padx=10, pady=5
        )
        reset_btn.pack(fill=tk.X, pady=5)

        exit_btn = tk.Button(
            button_frame, text="退出", command=self.root.quit,
            bg="#f44336", fg="white", font=("Arial", 10, "bold"),
            relief=tk.FLAT, padx=10, pady=5
        )
        exit_btn.pack(fill=tk.X, pady=5)

        # 右侧图片列表区域
        list_frame = tk.LabelFrame(
            main_frame, text="图片列表(可多选删除)", font=("Arial", 12),
            bg="#f0f0f0", fg="#333", padx=10, pady=10
        )
        list_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
        list_frame.columnconfigure(0, weight=1)
        list_frame.rowconfigure(0, weight=1)

        # 表格列配置
        columns = ('filename', 'original_size', 'format')
        self.tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=5)
        self.tree.heading('filename', text='文件名')
        self.tree.heading('original_size', text='原始尺寸')
        self.tree.heading('format', text='格式')
        self.tree.column('filename', width=200, stretch=True)
        self.tree.column('original_size', width=100, stretch=False)
        self.tree.column('format', width=80, stretch=False)

        # 滚动条
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        # 进度状态栏
        self.progress_var = tk.StringVar(value="就绪")
        progress_label = tk.Label(self.root, textvariable=self.progress_var, bg="#f0f0f0", fg="#555")
        progress_label.pack(pady=3)

    def delete_selected_img(self):
        """删除列表选中的图片"""
        selected = self.tree.selection()
        if not selected:
            messagebox.showinfo("提示", "请先在列表选中图片")
            return
        for item in selected:
            idx = self.tree.index(item)
            del self.image_paths[idx]
        self.refresh_image_list()

    def import_images(self):
        """批量导入图片"""
        supported_formats = [
            ("所有图片", "*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tiff;*.webp;*.ico"),
            ("JPG/JPEG", "*.jpg;*.jpeg"),
            ("PNG", "*.png"),
            ("BMP", "*.bmp"),
            ("GIF", "*.gif"),
            ("TIFF", "*.tiff"),
            ("WEBP", "*.webp"),
            ("全部文件", "*.*")
        ]
        filenames = filedialog.askopenfilenames(title="选择图片", filetypes=supported_formats)
        if not filenames:
            return

        add_count = 0
        for f in filenames:
            if f not in self.image_paths:
                self.image_paths.append(f)
                add_count += 1

        # 更新导入路径提示
        dirs = set(os.path.dirname(p) for p in filenames)
        if len(dirs) == 1:
            tip = f"目录:{os.path.basename(list(dirs)[0])} 新增{add_count}张,总计{len(self.image_paths)}张"
        else:
            tip = f"多目录导入,新增{add_count}张,总计{len(self.image_paths)}张"
        self.import_path_var.set(tip)
        self.refresh_image_list()

    def refresh_image_list(self):
        """刷新图片列表"""
        self.tree.delete(*self.tree.get_children())
        for path in self.image_paths:
            try:
                with Image.open(path) as img:
                    size_str = f"{img.width}×{img.height}"
                    fmt = img.format or "未知"
                    name = os.path.basename(path)
                    self.tree.insert("", tk.END, values=(name, size_str, fmt))
            except Exception as e:
                print(f"读取失败 {path}: {e}")
                self.tree.insert("", tk.END, values=(os.path.basename(path), "损坏", "-"))

    def start_processing(self):
        """批量处理图片核心逻辑"""
        # 前置校验
        if not self.image_paths:
            messagebox.showwarning("警告", "请先导入图片!")
            return
        if not self.output_path:
            self.output_path = filedialog.askdirectory(title="选择输出文件夹")
            if not self.output_path:
                return
            self.output_path_var.set(self.output_path)

        # 分辨率参数校验
        try:
            target_w = int(self.target_width.get())
            target_h = int(self.target_height.get())
        except ValueError:
            messagebox.showerror("错误", "宽高必须填写有效数字!")
            return
        if target_w <= 0 or target_h <= 0:
            messagebox.showerror("错误", "宽高必须大于0!")
            return

        success = 0
        fail = 0
        total = len(self.image_paths)

        # 遍历处理所有图片
        for idx, img_path in enumerate(self.image_paths):
            self.progress_var.set(f"处理中 {idx + 1}/{total}: {os.path.basename(img_path)}")
            self.root.update_idletasks()
            try:
                with Image.open(img_path) as img:
                    ow, oh = img.size
                    # 宽高比适配逻辑
                    if self.keep_aspect_ratio.get():
                        o_ratio = ow / oh
                        t_ratio = target_w / target_h
                        if o_ratio > t_ratio:
                            nw, nh = target_w, int(target_w / o_ratio)
                        else:
                            nh, nw = target_h, int(target_h * o_ratio)
                    else:
                        nw, nh = target_w, target_h

                    # 高清缩放
                    resized_img = img.resize((nw, nh), RESAMPLE_FILTER)
                    # 解析文件名与后缀
                    base = os.path.splitext(os.path.basename(img_path))[0]
                    ext = os.path.splitext(img_path)[1]
                    save_path = os.path.join(self.output_path, f"{base}{ext}")

                    # 防文件覆盖
                    cnt = 1
                    while os.path.exists(save_path):
                        save_path = os.path.join(self.output_path, f"{base}_resized{cnt}{ext}")
                        cnt += 1

                    # 分格式保存优化
                    save_kwargs = {"optimize": True}
                    if ext.lower() in (".jpg", ".jpeg"):
                        save_kwargs["quality"] = 95
                    if ext.lower() == ".png":
                        save_kwargs["compress_level"] = 6
                    resized_img.save(save_path, **save_kwargs)
                    success += 1
            except Exception as e:
                print(f"处理失败 {img_path}: {str(e)}")
                fail += 1

        # 处理完成提示
        self.progress_var.set(f"处理完成!成功{success}张,失败{fail}张")
        messagebox.showinfo("完成", f"成功:{success} 张\n失败:{fail} 张")

    def reset(self):
        """重置所有配置"""
        self.image_paths.clear()
        self.target_width.set("800")
        self.target_height.set("600")
        self.keep_aspect_ratio.set(True)
        self.output_path = ""
        self.import_path_var.set("未选择")
        self.output_path_var.set("未选择")
        self.progress_var.set("就绪")
        self.refresh_image_list()

    def select_output_path(self):
        """选择输出目录"""
        p = filedialog.askdirectory(title="选择输出文件夹")
        if p:
            self.output_path = p
            self.output_path_var.set(p)


if __name__ == "__main__":
    root = tk.Tk()
    app = ImageResizerApp(root)
    root.mainloop()

打包后的效果:

六、工具使用教程

七、项目优化总结与拓展方向

现有优化亮点

后续拓展方向

八、结语

本项目是一个轻量化、高实用、零成本的Python桌面实战项目,既适合新手学习Tkinter GUI开发、Pillow图像处理逻辑,也可直接作为日常办公工具落地使用。代码模块化清晰、注释完善、兼容性强,规避了绝大多数开源脚本的通病,兼顾实用性与学习价值。

到此这篇关于Python基于Tkinter打造高颜值批量图片缩放工具(可直接落地)的文章就介绍到这了,更多相关Python批量图片缩放内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文