【Python 实战】---- 实现一个可选择、配置操作的批量文件上传工具(四)配置管理界面和逻辑实现
1. 前言
很早之前,我实现过一个【Python 实战】---- 接口自动化:60行代码,如何通过Python requests实现图片上传,这个的确简化了前端开发对图片操作的时间,但是随着项目开发的越多,遇到一个新的问题,就是图片需要上传到每个项目的自己服务器中,而且每个项目的参数也不一样,有的需要加密,有的不需要,这就会出现我们需要根据不同的项目,对代码进行上传的微调,然后再打包成工具使用,最后就会发现有很多工具,完全达不到一个程序员的优雅,因此就想到使用 GUI 配置管理,将所有的工具进行配置管理。前边三篇文章其实已经将实现的核心已经完成,下边就讲解一下配置管理的实现。
1.1 实现效果
2. 配置管理 UI 界面
2.1 实现代码
ini
def __init__(self, parent, config_manager, main_gui):
self.parent = parent
self.config_manager = config_manager
self.main_gui = main_gui
self.window = tk.Toplevel(parent)
self.window.title("配置管理")
self.window.geometry("600x400")
self.window.transient(parent)
self.window.grab_set()
self.setup_ui()
self.refresh_config_list()
def setup_ui(self):
# 创建主框架
main_frame = ttk.Frame(self.window)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 配置列表框架
list_frame = ttk.LabelFrame(main_frame, text="配置列表")
list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 配置列表
self.config_listbox = tk.Listbox(list_frame)
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.config_listbox.yview)
self.config_listbox.configure(yscrollcommand=scrollbar.set)
self.config_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 按钮框架
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, padx=5, pady=5)
# 操作按钮
ttk.Button(button_frame, text="新建", command=self.new_config).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="编辑", command=self.edit_config).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="删除", command=self.delete_config).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="关闭", command=self.window.destroy).pack(side=tk.RIGHT, padx=5)
# 选择事件
self.config_listbox.bind('<<ListboxSelect>>', self.on_config_select)
2.2 实现效果

3. 刷新配置列表
3.1 实现分析
- 首先清空当前列表框中的所有内容(使用
self.config_listbox.delete(0, tk.END)
)。 - 调用配置管理器的
list_configs()
方法获取所有配置项的列表。 - 遍历每个配置项:
- 检查配置项是否有
title
字段。 - 如果有
title
,则将显示名称设置为title (name)
的格式,其中name
是配置项的名称。 - 如果没有
title
,则直接使用配置项的name
作为显示名称。
- 检查配置项是否有
- 将处理好的显示名称插入到列表框的末尾(使用
self.config_listbox.insert(tk.END, display_name)
),从而更新界面显示。
3.2 实现代码
python
def refresh_config_list(self):
"""刷新配置列表"""
self.config_listbox.delete(0, tk.END)
configs = self.config_manager.list_configs()
for config in configs:
title = config.get('title', '')
if title:
display_name = f"{title} ({config['name']})"
else:
display_name = config['name']
self.config_listbox.insert(tk.END, display_name)
4. 配置列表选择
4.1 实现分析
- 首先检查是否有选中的列表项(通过
self.config_listbox.curselection()
获取当前选择)。 - 如果有选中项,则获取选中项的索引,并再次获取所有配置列表。
- 根据索引从列表框中获取显示的文本内容。
- 由于列表框中的显示格式可能是
title (name)
或直接是name
,函数会解析这个显示文本以提取出真实的配置名称config_name
:- 如果文本包含
(
且以)
结尾,则从括号中提取配置名称。 - 否则直接使用该文本作为配置名称。
- 如果文本包含
- 使用提取到的配置名称调用配置管理器的
get_config
方法加载对应的配置数据。 - 如果成功获取到配置数据,则创建一个配置编辑窗口
ConfigEditWindow
实例,传入当前窗口、配置管理器、自身实例、配置数据和配置名称。 - 等待编辑窗口关闭(
self.window.wait_window(dialog.window)
)。 - 编辑窗口关闭后,调用
refresh_config_list
方法刷新配置列表的显示,以反映可能的更改。
4.2 实现代码
python
def on_config_select(self, event):
"""配置列表选择事件"""
selection = self.config_listbox.curselection()
if selection:
index = selection[0]
configs = self.config_manager.list_configs()
if index < len(configs):
# 获取实际的配置名称
selected_text = self.config_listbox.get(index)
# 从显示文本中提取配置名称(如果有title,则格式为"title (name)",否则为"name")
if ' (' in selected_text and selected_text.endswith(')'):
# 提取括号中的配置名称
config_name = selected_text.split(' (')[-1][:-1]
else:
# 直接使用配置名称
config_name = selected_text
config_data = self.config_manager.get_config(config_name)
if config_data:
# 打开编辑窗口
dialog = ConfigEditWindow(self.window, self.config_manager, self, config_data, config_name)
self.window.wait_window(dialog.window)
self.refresh_config_list()
5. 编辑配置
5.1 实现分析
- 首先检查配置列表框中是否已有选中的项(通过
self.config_listbox.curselection()
获取当前选择)。 - 如果没有选中任何项,则弹出一个警告对话框提示用户先选择一个配置,并结束函数执行。
- 如果有选中项,则获取选中项的显示文本内容。
- 由于列表框中的显示格式可能是
title (name)
或直接是name
,函数会解析这个显示文本以提取出真实的配置名称config_name
:- 如果文本包含
(
且以)
结尾,则从括号中提取配置名称。 - 否则直接使用该文本作为配置名称。
- 如果文本包含
- 使用提取到的配置名称调用配置管理器的
get_config
方法加载对应的配置数据。 - 如果成功获取到配置数据,则创建一个配置编辑窗口
ConfigEditWindow
实例,传入当前窗口、配置管理器、自身实例、配置数据和配置名称,从而打开编辑界面。 - 如果无法加载配置数据,则弹出错误对话框提示用户。
5.2 实现代码
python
def edit_config(self):
"""编辑配置"""
selection = self.config_listbox.curselection()
if not selection:
messagebox.showwarning("警告", "请先选择一个配置")
return
# 获取实际的配置名称
selected_text = self.config_listbox.get(selection[0])
# 从显示文本中提取配置名称(如果有title,则格式为"title (name)",否则为"name")
if ' (' in selected_text and selected_text.endswith(')'):
# 提取括号中的配置名称
config_name = selected_text.split(' (')[-1][:-1]
else:
# 直接使用配置名称
config_name = selected_text
config_data = self.config_manager.get_config(config_name)
if config_data:
ConfigEditWindow(self.window, self.config_manager, self, config_data, config_name)
else:
messagebox.showerror("错误", "无法加载配置数据")
6. 删除配置
6.1 实现分析
- 首先检查配置列表框中是否已有选中的项(通过
self.config_listbox.curselection()
获取当前选择)。 - 如果没有选中任何项,则弹出一个警告对话框提示用户先选择一个配置,并结束函数执行。
- 如果有选中项,则获取选中项的显示文本内容。
- 由于列表框中的显示格式可能是
title (name)
或直接是name
,函数会解析这个显示文本以提取出真实的配置名称config_name
:- 如果文本包含
(
且以)
结尾,则从括号中提取配置名称。 - 否则直接使用该文本作为配置名称。
- 如果文本包含
- 弹出一个确认对话框询问用户是否确定要删除该配置。
- 如果用户确认删除,则调用配置管理器的
delete_config
方法执行删除操作。 - 如果删除成功:
- 弹出成功提示对话框。
- 调用
refresh_config_list
方法刷新配置管理窗口的列表显示。 - 调用
self.main_gui.refresh_configs()
方法刷新主界面的配置下拉列表。
- 如果删除失败,则弹出错误对话框提示用户。
6.2 实现代码
python
def delete_config(self):
"""删除配置"""
selection = self.config_listbox.curselection()
if not selection:
messagebox.showwarning("警告", "请先选择一个配置")
return
# 获取实际的配置名称
selected_text = self.config_listbox.get(selection[0])
# 从显示文本中提取配置名称(如果有title,则格式为"title (name)",否则为"name")
if ' (' in selected_text and selected_text.endswith(')'):
# 提取括号中的配置名称
config_name = selected_text.split(' (')[-1][:-1]
else:
# 直接使用配置名称
config_name = selected_text
if messagebox.askyesno("确认", f"确定要删除配置 '{config_name}' 吗?"):
if self.config_manager.delete_config(config_name):
messagebox.showinfo("成功", "配置删除成功")
self.refresh_config_list()
self.main_gui.refresh_configs()
else:
messagebox.showerror("错误", "配置删除失败")
7. 配置界面和事件完成

8. 编辑配置界面
8.1 实现代码
ini
def __init__(self, parent, config_manager, list_window, config_data=None, config_name=None):
self.parent = parent
self.config_manager = config_manager
self.list_window = list_window
self.config_data = config_data or {}
self.config_name = config_name
self.window = tk.Toplevel(parent)
self.window.title("编辑配置" if config_name else "新建配置")
self.window.geometry("500x700")
self.window.transient(parent)
self.window.grab_set()
self.setup_ui()
if config_data:
self.load_config_data()
def setup_ui(self):
# 创建主框架
main_frame = ttk.Frame(self.window)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建Canvas和滚动条
canvas = tk.Canvas(main_frame)
scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
# 配置滚动区域
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# 布局Canvas和滚动条
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 配置名称(隐藏,仅在内部使用)
self.name_entry = ttk.Entry(scrollable_frame, width=50)
self.name_entry.pack_forget() # 隐藏配置名称输入框
# 如果是编辑模式,保留原始配置名称
if self.config_name:
self.name_entry.insert(0, self.config_name)
# URL
url_frame = ttk.Frame(scrollable_frame)
url_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(url_frame, text="上传URL:").pack(anchor=tk.W)
self.url_entry = ttk.Entry(url_frame, width=50)
self.url_entry.pack(fill=tk.X, padx=5)
# 文件字段名
file_field_frame = ttk.Frame(scrollable_frame)
file_field_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(file_field_frame, text="文件字段名:").pack(anchor=tk.W)
self.file_field_entry = ttk.Entry(file_field_frame, width=50)
self.file_field_entry.pack(fill=tk.X, padx=5)
# 文件类型字段名
file_type_field_frame = ttk.Frame(scrollable_frame)
file_type_field_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(file_type_field_frame, text="文件类型字段名:").pack(anchor=tk.W)
self.file_type_field_entry = ttk.Entry(file_type_field_frame, width=50)
self.file_type_field_entry.pack(fill=tk.X, padx=5)
# 名称字段名
name_field_frame = ttk.Frame(scrollable_frame)
name_field_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(name_field_frame, text="名称字段名:").pack(anchor=tk.W)
self.name_field_entry = ttk.Entry(name_field_frame, width=50)
self.name_field_entry.pack(fill=tk.X, padx=5)
# 名称值
name_value_frame = ttk.Frame(scrollable_frame)
name_value_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(name_value_frame, text="名称值:").pack(anchor=tk.W)
self.name_value_entry = ttk.Entry(name_value_frame, width=50)
self.name_value_entry.pack(fill=tk.X, padx=5)
# 成功代码
success_code_frame = ttk.Frame(scrollable_frame)
success_code_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(success_code_frame, text="成功代码:").pack(anchor=tk.W)
self.success_code_entry = ttk.Entry(success_code_frame, width=50)
self.success_code_entry.pack(fill=tk.X, padx=5)
# 内容字段名
content_field_frame = ttk.Frame(scrollable_frame)
content_field_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(content_field_frame, text="内容字段名:").pack(anchor=tk.W)
self.content_field_entry = ttk.Entry(content_field_frame, width=50)
self.content_field_entry.pack(fill=tk.X, padx=5)
# 前缀
prefix_frame = ttk.Frame(scrollable_frame)
prefix_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(prefix_frame, text="前缀:").pack(anchor=tk.W)
self.prefix_entry = ttk.Entry(prefix_frame, width=50)
self.prefix_entry.pack(fill=tk.X, padx=5)
# 标题
title_frame = ttk.Frame(scrollable_frame)
title_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(title_frame, text="标题:").pack(anchor=tk.W)
self.title_entry = ttk.Entry(title_frame, width=50)
self.title_entry.pack(fill=tk.X, padx=5)
# 请求头
headers_frame = ttk.Frame(scrollable_frame)
headers_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(headers_frame, text="请求头 (JSON格式):", foreground="gray").pack(anchor=tk.W)
self.headers_text = tk.Text(headers_frame, height=4, width=50)
self.headers_text.pack(fill=tk.X, padx=5)
# 数据
data_frame = ttk.Frame(scrollable_frame)
data_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(data_frame, text="数据 (JSON格式):", foreground="gray").pack(anchor=tk.W)
self.data_text = tk.Text(data_frame, height=4, width=50)
self.data_text.pack(fill=tk.X, padx=5)
# 按钮框架
button_frame = ttk.Frame(scrollable_frame)
button_frame.pack(fill=tk.X, padx=5, pady=5)
# 确认按钮
ttk.Button(button_frame, text="确认", command=self.save_config).pack(side=tk.LEFT, padx=5)
# 取消按钮
ttk.Button(button_frame, text="取消", command=self.cancel_config).pack(side=tk.RIGHT, padx=5)
8.2 实现效果

9. 编辑数据还原
9.1 实现分析
- 将配置名称(
config_name
)插入到名称输入框(name_entry
)中。 - 依次将配置数据中的各项参数插入到对应的输入控件中:
- URL地址插入到
url_entry
- 文件字段名(默认为'file')插入到
file_field_entry
- 文件类型字段名(默认为'fileType')插入到
file_type_field_entry
- 名称字段名(默认为'name')插入到
name_field_entry
- 名称字段值(默认为'file')插入到
name_value_entry
- 成功状态码(默认为200)转换为字符串后插入到
success_code_entry
- 内容字段名(默认为'content')插入到
content_field_entry
- URL地址插入到
- 将前缀配置(
prefix
)插入到前缀输入框(prefix_entry
)中。 - 处理请求头配置(
headers
):- 如果存在请求头配置,则将其转换为格式化的JSON字符串,并插入到请求头文本框(
headers_text
)中。
- 如果存在请求头配置,则将其转换为格式化的JSON字符串,并插入到请求头文本框(
- 处理数据配置(
data
):- 如果存在数据配置,则将其转换为格式化的JSON字符串,并插入到数据文本框(
data_text
)中。
- 如果存在数据配置,则将其转换为格式化的JSON字符串,并插入到数据文本框(
- 处理标题配置(
title
):- 如果存在标题配置,则将其插入到标题输入框(
title_entry
)中。
- 如果存在标题配置,则将其插入到标题输入框(
9.2 实现代码
python
def load_config_data(self):
"""加载配置数据到表单"""
self.name_entry.insert(0, self.config_name or "")
self.url_entry.insert(0, self.config_data.get('url', ''))
self.file_field_entry.insert(0, self.config_data.get('file_field_name', 'file'))
self.file_type_field_entry.insert(0, self.config_data.get('file_type_field', 'fileType'))
self.name_field_entry.insert(0, self.config_data.get('name_field', 'name'))
self.name_value_entry.insert(0, self.config_data.get('name_value', 'file'))
self.success_code_entry.insert(0, str(self.config_data.get('success_code', 200)))
self.content_field_entry.insert(0, self.config_data.get('content_field', 'content'))
# 加载前缀配置
self.prefix_entry.insert(0, self.config_data.get('prefix', ''))
# 加载请求头配置
headers = self.config_data.get('headers', {})
if headers:
import json
self.headers_text.insert("1.0", json.dumps(headers, indent=2, ensure_ascii=False))
# 加载数据配置
data = self.config_data.get('data', {})
if data:
import json
self.data_text.insert("1.0", json.dumps(data, indent=2, ensure_ascii=False))
# 加载title配置
title = self.config_data.get('title', '')
if title:
self.title_entry.insert(0, title)
10. 保存配置
10.1 实现分析
- 确定配置名称:
- 如果是新建配置(
self.config_name
为空),则生成一个32位的UUID作为配置名称。 - 如果是编辑现有配置,则使用原始的配置名称。
- 如果是新建配置(
- 验证并获取成功状态码:
- 从输入框中获取成功状态码,并尝试转换为整数。
- 如果转换失败(非数字),则弹出错误提示并终止保存操作。
- 构建配置数据字典
config_data
:- 从各个输入控件中获取URL、文件字段名、文件类型字段名、名称字段名、名称字段值、成功状态码、内容字段名、前缀和标题等基本配置项。
- 初始化请求头和数据配置为空字典。
- 处理请求头配置:
- 从文本框中获取请求头JSON字符串。
- 如果有内容,则尝试解析为JSON对象,如果解析失败则弹出错误提示并终止保存。
- 处理数据配置:
- 从文本框中获取数据JSON字符串。
- 如果有内容,则尝试解析为JSON对象,如果解析失败则弹出错误提示并终止保存。
- 保存配置到配置管理器:
- 如果是新建配置:
- 检查生成的配置名称是否已存在。
- 如果已存在,则询问用户是否覆盖,用户选择不覆盖则终止操作。
- 用户选择覆盖或配置不存在时,调用
add_config
或update_config
方法保存配置。
- 如果是编辑配置,则直接调用
update_config
方法更新配置。
- 如果是新建配置:
- 保存成功后:
- 弹出成功提示。
- 关闭当前编辑窗口。
- 刷新配置管理窗口的列表显示。
- 刷新主界面的配置下拉列表。
10.2 实现代码
python
def save_config(self):
"""保存配置"""
# 对于新建配置,总是生成32位唯一ID作为配置名称
if not self.config_name:
import uuid
name = str(uuid.uuid4()).replace('-', '')[:32]
else:
# 对于编辑配置,使用原始配置名称
name = self.config_name
try:
success_code = int(self.success_code_entry.get().strip())
except ValueError:
messagebox.showerror("错误", "成功代码必须是数字")
return
config_data = {
'url': self.url_entry.get().strip(),
'file_field_name': self.file_field_entry.get().strip(),
'file_type_field': self.file_type_field_entry.get().strip(),
'name_field': self.name_field_entry.get().strip(),
'name_value': self.name_value_entry.get().strip(),
'success_code': success_code,
'content_field': self.content_field_entry.get().strip(),
'prefix': self.prefix_entry.get().strip(),
'headers': {},
'data': {},
'title': self.title_entry.get().strip()
}
# 保存请求头配置
headers_text = self.headers_text.get("1.0", tk.END).strip()
if headers_text:
try:
import json
config_data['headers'] = json.loads(headers_text)
except Exception as e:
messagebox.showerror("错误", f"请求头格式错误: {e}")
return False
# 保存数据配置
data_text = self.data_text.get("1.0", tk.END).strip()
if data_text:
try:
import json
config_data['data'] = json.loads(data_text)
except Exception as e:
messagebox.showerror("错误", f"数据格式错误: {e}")
return False
# 如果是新建配置
if not self.config_name:
# 检查名称是否已存在
if self.config_manager.get_config(name):
# 询问用户是否覆盖现有配置
result = messagebox.askyesno("配置已存在", f"配置名称 '{name}' 已存在,是否覆盖?")
if not result:
return
# 如果用户选择覆盖,更新现有配置
self.config_manager.update_config(name, config_data)
else:
# 添加新配置
self.config_manager.add_config(name, config_data)
else:
# 更新现有配置
self.config_manager.update_config(self.config_name, config_data)
messagebox.showinfo("成功", "配置保存成功")
self.window.destroy()
self.list_window.refresh_config_list()
self.list_window.main_gui.refresh_configs()
11. 总结
配置管理和编辑配置界面其实都比较简单,就是简单的增删改查逻辑。下一篇就是将应用打包成 exe 就能直接作为工具使用了。