【Python 实战】---- 实现一个可选择、配置操作的批量文件上传工具(四)配置管理界面和逻辑实现

【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 实现分析

  1. 首先清空当前列表框中的所有内容(使用 self.config_listbox.delete(0, tk.END))。
  2. 调用配置管理器的 list_configs() 方法获取所有配置项的列表。
  3. 遍历每个配置项:
    • 检查配置项是否有 title 字段。
    • 如果有 title,则将显示名称设置为 title (name) 的格式,其中 name 是配置项的名称。
    • 如果没有 title,则直接使用配置项的 name 作为显示名称。
  4. 将处理好的显示名称插入到列表框的末尾(使用 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 实现分析

  1. 首先检查是否有选中的列表项(通过 self.config_listbox.curselection() 获取当前选择)。
  2. 如果有选中项,则获取选中项的索引,并再次获取所有配置列表。
  3. 根据索引从列表框中获取显示的文本内容。
  4. 由于列表框中的显示格式可能是 title (name) 或直接是 name,函数会解析这个显示文本以提取出真实的配置名称 config_name
    • 如果文本包含 ( 且以 ) 结尾,则从括号中提取配置名称。
    • 否则直接使用该文本作为配置名称。
  5. 使用提取到的配置名称调用配置管理器的 get_config 方法加载对应的配置数据。
  6. 如果成功获取到配置数据,则创建一个配置编辑窗口 ConfigEditWindow 实例,传入当前窗口、配置管理器、自身实例、配置数据和配置名称。
  7. 等待编辑窗口关闭(self.window.wait_window(dialog.window))。
  8. 编辑窗口关闭后,调用 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 实现分析

  1. 首先检查配置列表框中是否已有选中的项(通过 self.config_listbox.curselection() 获取当前选择)。
  2. 如果没有选中任何项,则弹出一个警告对话框提示用户先选择一个配置,并结束函数执行。
  3. 如果有选中项,则获取选中项的显示文本内容。
  4. 由于列表框中的显示格式可能是 title (name) 或直接是 name,函数会解析这个显示文本以提取出真实的配置名称 config_name
    • 如果文本包含 ( 且以 ) 结尾,则从括号中提取配置名称。
    • 否则直接使用该文本作为配置名称。
  5. 使用提取到的配置名称调用配置管理器的 get_config 方法加载对应的配置数据。
  6. 如果成功获取到配置数据,则创建一个配置编辑窗口 ConfigEditWindow 实例,传入当前窗口、配置管理器、自身实例、配置数据和配置名称,从而打开编辑界面。
  7. 如果无法加载配置数据,则弹出错误对话框提示用户。

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 实现分析

  1. 首先检查配置列表框中是否已有选中的项(通过 self.config_listbox.curselection() 获取当前选择)。
  2. 如果没有选中任何项,则弹出一个警告对话框提示用户先选择一个配置,并结束函数执行。
  3. 如果有选中项,则获取选中项的显示文本内容。
  4. 由于列表框中的显示格式可能是 title (name) 或直接是 name,函数会解析这个显示文本以提取出真实的配置名称 config_name
    • 如果文本包含 ( 且以 ) 结尾,则从括号中提取配置名称。
    • 否则直接使用该文本作为配置名称。
  5. 弹出一个确认对话框询问用户是否确定要删除该配置。
  6. 如果用户确认删除,则调用配置管理器的 delete_config 方法执行删除操作。
  7. 如果删除成功:
    • 弹出成功提示对话框。
    • 调用 refresh_config_list 方法刷新配置管理窗口的列表显示。
    • 调用 self.main_gui.refresh_configs() 方法刷新主界面的配置下拉列表。
  8. 如果删除失败,则弹出错误对话框提示用户。

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 实现分析

  1. 将配置名称(config_name)插入到名称输入框(name_entry)中。
  2. 依次将配置数据中的各项参数插入到对应的输入控件中:
    • 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
  3. 将前缀配置(prefix)插入到前缀输入框(prefix_entry)中。
  4. 处理请求头配置(headers):
    • 如果存在请求头配置,则将其转换为格式化的JSON字符串,并插入到请求头文本框(headers_text)中。
  5. 处理数据配置(data):
    • 如果存在数据配置,则将其转换为格式化的JSON字符串,并插入到数据文本框(data_text)中。
  6. 处理标题配置(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 实现分析

  1. 确定配置名称:
    • 如果是新建配置(self.config_name 为空),则生成一个32位的UUID作为配置名称。
    • 如果是编辑现有配置,则使用原始的配置名称。
  2. 验证并获取成功状态码:
    • 从输入框中获取成功状态码,并尝试转换为整数。
    • 如果转换失败(非数字),则弹出错误提示并终止保存操作。
  3. 构建配置数据字典 config_data
    • 从各个输入控件中获取URL、文件字段名、文件类型字段名、名称字段名、名称字段值、成功状态码、内容字段名、前缀和标题等基本配置项。
    • 初始化请求头和数据配置为空字典。
  4. 处理请求头配置:
    • 从文本框中获取请求头JSON字符串。
    • 如果有内容,则尝试解析为JSON对象,如果解析失败则弹出错误提示并终止保存。
  5. 处理数据配置:
    • 从文本框中获取数据JSON字符串。
    • 如果有内容,则尝试解析为JSON对象,如果解析失败则弹出错误提示并终止保存。
  6. 保存配置到配置管理器:
    • 如果是新建配置:
      • 检查生成的配置名称是否已存在。
      • 如果已存在,则询问用户是否覆盖,用户选择不覆盖则终止操作。
      • 用户选择覆盖或配置不存在时,调用 add_configupdate_config 方法保存配置。
    • 如果是编辑配置,则直接调用 update_config 方法更新配置。
  7. 保存成功后:
    • 弹出成功提示。
    • 关闭当前编辑窗口。
    • 刷新配置管理窗口的列表显示。
    • 刷新主界面的配置下拉列表。

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 就能直接作为工具使用了。

相关推荐
杜子不疼.3 分钟前
《Python学习之第三方库:开启无限可能》
开发语言·python·学习
青衫客361 小时前
用 Python 实现一个“小型 ReAct 智能体”:思维链 + 工具调用 + 环境交互
python·大模型·llm·react
AI视觉网奇1 小时前
音频分类模型笔记
人工智能·python·深度学习
Ratten2 小时前
【Python 实战】---- 实现一个可选择、配置操作的批量文件上传工具(五)打包成 exe 应用
python
跟橙姐学代码2 小时前
写 Python 函数别再死抠参数了,这招让代码瞬间灵活
前端·python
nightunderblackcat3 小时前
进阶向:人物关系三元组,解锁人物关系网络的钥匙
开发语言·python·开源·php
站大爷IP3 小时前
Pandas与NumPy:Python数据处理的双剑合璧
python
站大爷IP4 小时前
Python枚举进化论:IntEnum与StrEnum的实战指南
python
甄超锋4 小时前
python sqlite3模块
jvm·数据库·python·测试工具·django·sqlite·flask