前言

在上一篇文章《宝藏级教程:把你的 Python 脚本打包成 exe,拿给同事用直接秀翻全场!》中,我们学会了如何将 Python 脚本打包成 .exe 文件。但命令行工具终究不够直观。如果我们能给它穿上一件“图形界面”的外衣,那体验将是质的飞跃!

本文将作为进阶篇,手把手带你用 Python 内置的 tkinter 库,将我们之前的文件重命名逻辑,封装成一个功能强大、界面友好的桌面应用。最终效果如下:

你将学到:

  • 如何用 tkinter 搭建一个美观的 GUI 布局。

  • 如何使用单选框、复选框、文本输入框等多种UI组件。

  • 如何实现文件选择、日志实时输出等高级功能。

  • 一个关键技巧:如何防止 GUI 在处理大量文件时“卡死”。

最重要的是,整个过程完全不需要安装任何第三方库,Python 自带的就够了!

文章目录

前言

文章目录

一、 项目规划:设计我们的工具界面

二、 代码实现:分步详解

1. 初始化:搭建窗口和布局框架

2. 填充组件:让界面丰满起来

3. 核心逻辑:响应用户操作

选择文件夹 (select_folder)

开始处理 (start_processing)

4. 终极秘诀:用多线程防止UI卡死

三、 完整代码与打包

完整脚本

打包成 .exe

总结


一、 项目规划:设计我们的工具界面

在动手写代码之前,我们先规划一下工具的蓝图。一个好的布局是成功的一半。我们可以把界面拆分成五个逻辑区域:

  1. 操作选择区:用单选框,让用户选择是“添加”还是“移除”前缀。

  2. 前缀定义区:用一个文本输入框,让用户自定义前缀内容。

  3. 文件类型区:用一组复选框,让用户自由勾选要处理的文件格式。

  4. 目标选择区:一个按钮,点击后弹出窗口让用户选择要处理的文件夹。

  5. 执行与日志区:一个“开始”按钮来启动任务,以及一个文本框来实时显示处理日志。

有了这个清晰的蓝图,我们就可以开始用代码把它变为现实了。


二、 代码实现:分步详解

我们将通过一个 BatchFileRenamer 类来封装整个应用。下面是核心代码的逐一解析。

1. 初始化:搭建窗口和布局框架

我们首先需要创建主窗口,并规划好五个功能区的基本布局。这里我们大量使用 ttk.LabelFrame,它能帮我们创建带标题的分组框,让界面井井有条。

import os
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext
import threading

class BatchFileRenamer:
    def __init__(self, root):
        self.root = root
        self.root.title("文件批量重命名工具 Pro v1.1")
        self.root.geometry("650x550") # 设置窗口大小

        # --- 主框架 ---
        main_frame = ttk.Frame(root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # --- 区域1:操作选择 ---
        action_frame = ttk.LabelFrame(main_frame, text="第一步:选择操作", padding="10")
        action_frame.pack(fill=tk.X, pady=5)
        # ... (组件代码稍后填充)

        # --- 区域2:自定义前缀 ---
        prefix_frame = ttk.LabelFrame(main_frame, text="第二步:输入前缀文本", padding="10")
        prefix_frame.pack(fill=tk.X, pady=5)
        # ...

        # --- 区域3:文件类型选择 ---
        ext_frame = ttk.LabelFrame(main_frame, text="第三步:勾选要处理的文件类型", padding="10")
        ext_frame.pack(fill=tk.X, pady=5)
        # ...

        # --- 区域4:选择文件夹 ---
        folder_frame = ttk.LabelFrame(main_frame, text="第四步:选择目标文件夹", padding="10")
        folder_frame.pack(fill=tk.X, pady=5)
        # ...

        # --- 区域5:执行与日志 ---
        run_frame = ttk.Frame(main_frame, padding="10")
        run_frame.pack(fill=tk.X, pady=10)
        # ...

        log_frame = ttk.LabelFrame(main_frame, text="处理日志", padding="10")
        log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        # ...

2. 填充组件:让界面丰满起来

现在,我们向每个框架中填充具体的 UI 组件,比如 Radiobutton(单选框)、Entry(输入框)、Checkbutton(复选框)和 Button。

# --- 区域1:操作选择组件 ---
self.action_var = tk.StringVar(value="add")
ttk.Radiobutton(action_frame, text="添加前缀", variable=self.action_var, value="add").pack(side=tk.LEFT, padx=5)
ttk.Radiobutton(action_frame, text="移除前缀", variable=self.action_var, value="remove").pack(side=tk.LEFT, padx=5)

# --- 区域2:自定义前缀组件 ---
self.prefix_entry = ttk.Entry(prefix_frame, width=50)
self.prefix_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.prefix_entry.insert(0, "(公开)") # 设置默认值

# --- 区域3:文件类型选择组件 ---
self.ext_vars = {}
extensions = ['.doc', '.docx', '.xls', '.xlsx', '.pdf', '.ppt', '.pptx', '.txt']
for i, ext in enumerate(extensions):
    var = tk.BooleanVar(value=True) # 默认选中
    self.ext_vars[ext] = var
    row, col = divmod(i, 4) # 每4个换一行
    cb = ttk.Checkbutton(ext_frame, text=ext, variable=var)
    cb.grid(row=row, column=col, padx=5, pady=2, sticky='w')

# --- 区域4:选择文件夹组件 ---
self.folder_path = tk.StringVar()
ttk.Button(folder_frame, text="选择文件夹...", command=self.select_folder).pack(side=tk.LEFT, padx=5)
ttk.Label(folder_frame, textvariable=self.folder_path, foreground="blue").pack(side=tk.LEFT, padx=5)

# --- 区域5:执行与日志组件 ---
self.run_button = ttk.Button(run_frame, text="开始执行", command=self.start_processing)
self.run_button.pack()
self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, font=("Consolas", 9))
self.log_text.pack(fill=tk.BOTH, expand=True)

3. 核心逻辑:响应用户操作

界面搭好了,但它还是个“花架子”。我们需要编写函数来响应用户的点击事件。

选择文件夹 (select_folder)

当用户点击“选择文件夹...”按钮时,调用 filedialog.askdirectory 弹出标准的文件夹选择窗口。

codePython

def select_folder(self):
    path = filedialog.askdirectory(title="请选择要处理的顶层文件夹")
    if path:
        self.folder_path.set(path)
        self.log("已选择文件夹: " + path)

def log(self, message):
    self.log_text.insert(tk.END, message + "\n")
    self.log_text.see(tk.END) # 自动滚动到日志末尾
开始处理 (start_processing)

这是总指挥。当用户点击“开始执行”时,它负责:

  1. 从界面上收集所有用户的设置(操作类型、前缀、文件类型等)。

  2. 进行基本校验,确保用户已选择文件夹。

  3. 启动一个新线程来执行耗时的文件重命名任务。

codePython

def start_processing(self):
    folder = self.folder_path.get()
    prefix = self.prefix_entry.get()
    # ... 获取其他设置 ...

    if not folder:
        self.log("错误:请先选择一个文件夹!")
        return
    # ... 其他校验 ...

    self.run_button.config(state=tk.DISABLED) # 禁用按钮,防止重复点击
    
    # 关键!在新线程中运行核心逻辑,避免UI卡死
    thread = threading.Thread(target=self._process_files_thread, args=(...))
    thread.start()

4. 终极秘诀:用多线程防止UI卡死

这是本教程最重要的知识点! 如果直接在主线程中进行文件扫描和重命名,当文件数量很多时,整个程序的界面会卡住不动,变成“未响应”状态。

正确的做法是,将这些耗时操作放到一个单独的**线程(Thread)**中去执行。主线程继续负责界面的刷新和响应,而子线程在后台默默地干活。这样即使用户处理上万个文件,界面依然流畅丝滑。

后台线程函数 _process_files_thread 的逻辑和我们之前的命令行脚本几乎一样,只是把 print 改成了调用 self.log 方法,从而将日志输出到界面的文本框中。

codePython

def _process_files_thread(self, folder, prefix, action, extensions):
    self.log(f"--- 操作开始 ---")
    count = 0
    try:
        # os.walk 遍历所有文件和子文件夹
        for root, dirs, files in os.walk(folder):
            for filename in files:
                # ... (这里是添加/移除前缀的核心判断和 os.rename 逻辑)
                # ... (将 print 改为 self.log)
                ...
    except Exception as e:
        self.log(f"发生严重错误: {e}")
    finally:
        self.log(f"--- 操作完成!共处理了 {count} 个文件。---")
        self.run_button.config(state=tk.NORMAL) # 任务结束,恢复按钮

三、 完整代码与打包

将以上所有部分组合起来,就是我们功能完善的桌面工具了。

完整脚本

codePython

# (在此处粘贴文章开头提供的完整Python代码)
import os
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext
import threading

class BatchFileRenamer:
    def __init__(self, root):
        self.root = root
        self.root.title("文件批量重命名工具 Pro v1.1")
        self.root.geometry("650x550")

        # --- 样式 ---
        style = ttk.Style()
        style.configure("TLabel", font=("Microsoft YaHei", 10))
        style.configure("TButton", font=("Microsoft YaHei", 10))
        style.configure("TRadiobutton", font=("Microsoft YaHei", 10))
        style.configure("TCheckbutton", font=("Microsoft YaHei", 10))
        style.configure("TEntry", font=("Microsoft YaHei", 10))

        # --- 主框架 ---
        main_frame = ttk.Frame(root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # --- 1. 操作选择 ---
        action_frame = ttk.LabelFrame(main_frame, text="第一步:选择操作", padding="10")
        action_frame.pack(fill=tk.X, pady=5)

        self.action_var = tk.StringVar(value="add")
        ttk.Radiobutton(action_frame, text="添加前缀", variable=self.action_var, value="add").pack(side=tk.LEFT, padx=5)
        ttk.Radiobutton(action_frame, text="移除前缀", variable=self.action_var, value="remove").pack(side=tk.LEFT, padx=5)

        # --- 2. 自定义前缀 ---
        prefix_frame = ttk.LabelFrame(main_frame, text="第二步:输入前缀文本", padding="10")
        prefix_frame.pack(fill=tk.X, pady=5)

        ttk.Label(prefix_frame, text="前缀:").pack(side=tk.LEFT, padx=5)
        self.prefix_entry = ttk.Entry(prefix_frame, width=50)
        self.prefix_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        self.prefix_entry.insert(0, "(公开)")

        # --- 3. 文件类型选择 ---
        ext_frame = ttk.LabelFrame(main_frame, text="第三步:勾选要处理的文件类型", padding="10")
        ext_frame.pack(fill=tk.X, pady=5)
        
        self.ext_vars = {}
        extensions = ['.doc', '.docx', '.xls', '.xlsx', '.pdf', '.ppt', '.pptx', '.txt'] 
        
        for i, ext in enumerate(extensions):
            var = tk.BooleanVar(value=True)
            self.ext_vars[ext] = var
            # 每4个换一行
            row, col = divmod(i, 4)
            cb = ttk.Checkbutton(ext_frame, text=ext, variable=var)
            cb.grid(row=row, column=col, padx=5, pady=2, sticky='w')

        # --- 4. 选择文件夹 ---
        folder_frame = ttk.LabelFrame(main_frame, text="第四步:选择目标文件夹", padding="10")
        folder_frame.pack(fill=tk.X, pady=5)

        self.folder_path = tk.StringVar()
        ttk.Button(folder_frame, text="选择文件夹...", command=self.select_folder).pack(side=tk.LEFT, padx=5)
        ttk.Label(folder_frame, textvariable=self.folder_path, foreground="blue").pack(side=tk.LEFT, padx=5)

        # --- 5. 执行与日志 ---
        run_frame = ttk.Frame(main_frame, padding="10")
        run_frame.pack(fill=tk.X, pady=10)

        self.run_button = ttk.Button(run_frame, text="开始执行", command=self.start_processing)
        self.run_button.pack()

        log_frame = ttk.LabelFrame(main_frame, text="处理日志", padding="10")
        log_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, font=("Consolas", 9))
        self.log_text.pack(fill=tk.BOTH, expand=True)

    def select_folder(self):
        path = filedialog.askdirectory(title="请选择要处理的顶层文件夹")
        if path:
            self.folder_path.set(path)
            self.log("已选择文件夹: " + path)

    def log(self, message):
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.root.update_idletasks()

    def start_processing(self):
        folder = self.folder_path.get()
        prefix = self.prefix_entry.get()
        action = self.action_var.get()
        selected_extensions = [ext for ext, var in self.ext_vars.items() if var.get()]

        if not folder:
            self.log("错误:请先选择一个文件夹!")
            return
        if not prefix:
            self.log("错误:请输入要操作的前缀文本!")
            return
        if not selected_extensions:
            self.log("错误:请至少勾选一个文件类型!")
            return

        self.run_button.config(state=tk.DISABLED)
        self.log_text.delete('1.0', tk.END)
        
        thread = threading.Thread(target=self._process_files_thread, args=(folder, prefix, action, selected_extensions))
        thread.start()

    def _process_files_thread(self, folder, prefix, action, extensions):
        self.log(f"--- 操作开始 ---")
        self.log(f"操作类型: {'添加前缀' if action == 'add' else '移除前缀'}")
        self.log(f"前缀文本: '{prefix}'")
        self.log(f"目标文件类型: {', '.join(extensions)}")
        self.log(f"目标文件夹: '{folder}'")
        self.log("-" * 30)

        count = 0
        extensions_tuple = tuple(extensions)

        try:
            for root, dirs, files in os.walk(folder):
                for filename in files:
                    if not filename.lower().endswith(extensions_tuple):
                        continue

                    if action == "add" and not filename.startswith(prefix):
                        new_filename = prefix + filename
                        old_path = os.path.join(root, filename)
                        new_path = os.path.join(root, new_filename)
                        try:
                            os.rename(old_path, new_path)
                            self.log(f"✅ 添加成功: {filename} -> {new_filename}")
                            count += 1
                        except Exception as e:
                            self.log(f"❌ 添加失败: {filename}, 原因: {e}")
                    
                    elif action == "remove" and filename.startswith(prefix):
                        old_path = os.path.join(root, filename)
                        new_filename = filename
                        while new_filename.startswith(prefix):
                            new_filename = new_filename[len(prefix):]
                        
                        new_path = os.path.join(root, new_filename)
                        try:
                            os.rename(old_path, new_path)
                            self.log(f"✅ 移除成功: {filename} -> {new_filename}")
                            count += 1
                        except Exception as e:
                            self.log(f"❌ 移除失败: {filename}, 原因: {e}")

            self.log("-" * 30)
            self.log(f"--- 操作完成!共处理了 {count} 个文件。---")
        except Exception as e:
            self.log(f"发生严重错误: {e}")
        finally:
            self.run_button.config(state=tk.NORMAL)

if __name__ == '__main__':
    root = tk.Tk()
    app = BatchFileRenamer(root)
    root.mainloop()

打包成 .exe

打包 GUI 应用的关键是使用 --windowed (或 -w) 参数来隐藏黑色的命令行窗口。

codeBash

# 将 file_tool_pro.py 替换成你的脚本名
pyinstaller --onefile --windowed --icon="my_icon.ico" file_tool_pro.py

总结

恭喜你!通过本教程,你不仅学会了如何用 tkinter 构建一个完整的桌面应用,还掌握了防止界面卡死的多线程编程技巧。tkinter 虽然简单,但功能强大,足以应对大部分中小型桌面工具的开发需求。

现在,你已经拥有一个完全属于自己的、专业的、可分享的文件处理工具了。快去试试给它增加更多新功能吧,比如添加后缀、修改文件名中的特定文字等等,天空才是你的极限!

更多推荐