在自动化脚本开发中,经常需要让用户选择文件或文件夹作为输入源。传统命令行方式对非技术人员不够友好,而PySimpleGUI这个"Python界的瑞士军刀"能让我们用极简代码实现专业级GUI交互。今天我们就用5分钟,从零构建一个带完整文件选择功能的生产力工具。
首先确保Python版本≥3.10(推荐使用3.11+获得更好的类型提示支持)。安装PySimpleGUI只需一行命令:
bash复制pip install pysimplegui --upgrade
基础窗口框架代码如下,这个模板可以保存为gui_template.py反复使用:
python复制import PySimpleGUI as sg
# 主题设置(可选)
sg.theme('SystemDefault') # 自动适配系统主题
def main():
layout = [[]] # 布局将在后续填充
window = sg.Window('文件处理工具', layout, finalize=True)
while True:
event, values = window.read()
if event == sg.WIN_CLOSED:
break
window.close()
if __name__ == '__main__':
main()
关键点说明:
finalize=True确保窗口立即显示while True保持持续响应sg.WIN_CLOSED处理窗口关闭事件PySimpleGUI提供四种文件选择方案,我们重点实现最常用的三种:
python复制[
sg.Text("选择文件:"),
sg.Input(key="-FILE-", enable_events=True),
sg.FileBrowse(
button_text="浏览...",
target="-FILE-",
file_types=(("文本文件", "*.txt"), ("所有文件", "*.*")),
initial_folder="~/Documents" # 设置默认目录
)
]
实用技巧:
enable_events=True使得输入框内容变化时触发事件file_types参数支持多重过滤条件os.path.expanduser("~")获取家目录python复制[
sg.Text("批量选择:"),
sg.Multiline(key="-FILES-", size=(30,3)),
sg.FilesBrowse(
button_text="多选...",
target="-FILES-",
file_types=(("CSV文件", "*.csv"),)
)
]
注意:多选结果会以分号分隔,可用
values["-FILES-"].split(";")转换为列表
python复制[
sg.Text("输出目录:"),
sg.Input(key="-FOLDER-"),
sg.FolderBrowse(
button_text="选择文件夹",
target="-FOLDER-",
initial_folder="/tmp" # 临时目录示例
)
]
完整布局示例:
python复制layout = [
[sg.Text("文件处理工具 v1.0", font=("Arial", 16))],
# 单文件选择行
[sg.Text("选择文件:"), sg.Input(key="-FILE-"), sg.FileBrowse()],
# 多文件选择行
[sg.Text("批量选择:"), sg.Multiline(key="-FILES-", size=(40,3)), sg.FilesBrowse()],
# 文件夹选择行
[sg.Text("输出目录:"), sg.Input(key="-FOLDER-"), sg.FolderBrowse()],
[sg.Button("处理"), sg.Button("退出")]
]
获取路径后需要进行规范化处理,避免常见问题:
python复制import os.path
def process_file(path):
try:
# 路径标准化处理
clean_path = os.path.normpath(path)
if not os.path.exists(clean_path):
sg.popup_error(f"路径不存在: {clean_path}")
return False
# 获取文件信息
file_stats = os.stat(clean_path)
sg.popup_notify(
f"文件大小: {file_stats.st_size/1024:.2f}KB\n"
f"修改时间: {file_stats.st_mtime}"
)
return True
except Exception as e:
sg.popup_error(f"处理失败: {str(e)}")
return False
在事件循环中调用:
python复制while True:
event, values = window.read()
if event == "处理":
if values["-FILE-"]:
process_file(values["-FILE-"])
# ...其他事件处理
PySimpleGUI原生不支持拖放,但可以通过tkinter实现:
python复制def make_draggable(window, key):
widget = window[key].Widget
widget.bind("<DragEnter>", lambda e: e.widget.config(background='#e0e0ff'))
widget.bind("<DragLeave>", lambda e: e.widget.config(background='white'))
widget.bind("<B1-Motion>", lambda e: None) # 禁用内部拖动
widget.bind("<ButtonRelease-1>",
lambda e: window.write_event_value("-DROP-",
window.TKroot.tk.splitlist(e.widget.tk.call('tk_getOpenFile',
initialdir=os.path.dirname(e.widget.get())))))
使用json保存历史记录:
python复制import json
HISTORY_FILE = "recent_files.json"
def load_history():
try:
with open(HISTORY_FILE) as f:
return json.load(f).get("files", [])
except:
return []
def save_history(files):
with open(HISTORY_FILE, "w") as f:
json.dump({"files": files[-10:]}, f) # 只保留最近10条
防止界面卡顿的正确姿势:
python复制import threading
def long_running_task(path):
# 模拟耗时操作
import time
time.sleep(5)
return f"处理完成: {path}"
def start_worker(window, path):
def worker():
result = long_running_task(path)
window.write_event_value("-TASK_DONE-", result)
threading.Thread(target=worker, daemon=True).start()
在事件循环中:
python复制if event == "处理":
start_worker(window, values["-FILE-"])
elif event == "-TASK_DONE-":
sg.popup(values[event])
使用PyInstaller打包为独立可执行文件:
bash复制pip install pyinstaller
build.spec:python复制# -*- mode: python -*-
from PyInstaller.utils.hooks import collect_data_files
a = Analysis(
['main.py'],
datas=collect_data_files('pysimplegui'),
...
)
bash复制pyinstaller --onefile --windowed --icon=app.ico build.spec
打包优化技巧:
--noconsole隐藏命令行窗口--add-data包含额外资源文件python复制def create_log_analyzer():
layout = [
[sg.Text("日志分析工具", font=("Arial", 14))],
[sg.Multiline(size=(60,15), key="-LOG-")],
[
sg.Button("打开日志"),
sg.Button("错误统计"),
sg.Button("时间分布"),
sg.Exit()
]
]
window = sg.Window("日志分析", layout)
while True:
event, values = window.read()
if event == "打开日志":
filename = sg.popup_get_file("选择日志文件")
if filename:
with open(filename) as f:
window["-LOG-"].update(f.read())
python复制from PIL import Image
def batch_resize_images(file_list, output_dir, size=(800,600)):
for file in file_list:
try:
img = Image.open(file)
img.thumbnail(size)
out_path = os.path.join(output_dir, f"resized_{os.path.basename(file)}")
img.save(out_path)
except Exception as e:
print(f"处理失败 {file}: {str(e)}")
延迟加载:对于复杂界面,可以使用Column元素的visible属性实现标签页效果
元素复用:避免频繁创建销毁窗口,改用window.hide()和window.un_hide()
内存管理:处理大文件时使用生成器逐行读取:
python复制def read_large_file(path):
with open(path, 'rb') as f:
for line in f:
yield line
python复制sg.theme('SystemDefaultForReal') # 最轻量级的主题
路径分隔符:始终使用os.path.join()代替硬编码的/或\
权限问题:在Linux/macOS上检查写权限:
python复制if not os.access(folder, os.W_OK):
sg.popup_error("无写入权限")
xml复制<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</windowsSettings>
</application>
python复制if sg.running_mac():
layout.insert(0, [sg.MenubarCustom([['File', ['Exit']]])])