控制的TComboBox下拉框的宽度

本文介绍了一个用于调整Delphi中组合框下拉宽度的过程。通过计算最长项的宽度并据此设置下拉宽度,确保所有选项都能完全展示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

{自动显示下拉框的宽度}
procedure TdeliveOrderForm.comb_productDropDown(Sender: TObject);
var
CBox: TComboBox;
Width: Integer;
I, TextLen: Longint;
lf: LOGFONT;
f: HFONT;
begin
CBox := (Sender as TComboBox);
Width := CBox.Width;
FillChar(lf,SizeOf(lf),0);
StrPCopy(lf.lfFaceName, CBox.Font.Name);
lf.lfHeight := CBox.Font.Height;
lf.lfWeight := FW_NORMAL;
if fsBold in CBox.Font.Style then
lf.lfWeight := lf.lfWeight or FW_BOLD;

f := CreateFontIndirect(lf);
if (f <> 0) then
try
CBox.Canvas.Handle := GetDC(CBox.Handle);
SelectObject(CBox.Canvas.Handle,f);
try
for i := 0 to CBox.Items.Count-1 do begin
TextLen := CBox.Canvas.TextWidth(CBox.Items[i]);
if CBox.Items.Count-1 > CBox.DropDownCount then
begin
if TextLen > Width-25 then
Width := TextLen +25;
end
else if CBox.Items.Count-1 <= CBox.DropDownCount then
begin
if TextLen > Width-5 then
Width := TextLen+8;
end;
end;
finally
ReleaseDC(CBox.Handle, CBox.Canvas.Handle);
end;
finally
DeleteObject(f);
end;
SendMessage(CBox.Handle, CB_SETDROPPEDWIDTH, Width, 0);
end;
import tkinter as tk from tkinter import filedialog, messagebox, ttk, scrolledtext import csv from datetime import datetime import logging import os from collections import defaultdict class CSVProcessorApp: def __init__(self, root): self.root = root self.root.title("CSV_ProcessPro") self.root.geometry("800x600") self.root.resizable(False, False) # 初始化变量 self.file_path = tk.StringVar() self.csv_data = [] self.headers = [] self.raw_data = [] # 存储原始数据 self.header_row_index = tk.IntVar(value=0) # 表头行索引 self.setup_variables() self.setup_logging() self.create_widgets() self.setup_styles() def setup_styles(self): """设置全局样式""" self.style = ttk.Style() self.style.configure("TFrame", background="#f0f0f0") self.style.configure("TLabel", background="#f0f0f0", font=('Arial', 9)) self.style.configure("TButton", font=('Arial', 9, 'bold')) self.style.configure("Accent.TButton", foreground="black", font=('Arial', 9, 'bold'), borderwidth=2, relief="raised") self.style.map("Accent.TButton", background=[("active", "#4a90e2"), ("!active", "#d4e6ff")], bordercolor=[("active", "#4a90e2"), ("!active", "#ffcc00")]) self.style.configure("Remove.TButton", foreground="black", font=('Arial', 8), background="#ffcccc", borderwidth=1, relief="solid") self.style.map("Remove.TButton", background=[("active", "#ff9999"), ("!active", "#ffcccc")]) self.style.configure("Header.TCombobox", font=('Arial', 9)) def setup_variables(self): """初始化所有动态变量""" # 排序相关 self.sort_header = tk.StringVar() self.sort_order = tk.StringVar(value="升序") # 去重相关 self.dedupe_header = tk.StringVar() # 删除行相关 self.delete_keyword = tk.StringVar() self.delete_column = tk.StringVar() self.delete_case_sensitive = tk.BooleanVar() # 合并文件相关 self.merge_file_paths = [] self.merge_column = tk.StringVar() # 状态变量 self.enable_sort = tk.BooleanVar() self.enable_dedupe = tk.BooleanVar() self.enable_custom_letter_sort = tk.BooleanVar() self.letter_range_start = tk.StringVar(value="A") self.letter_range_end = tk.StringVar(value="Z") # 组合处理相关 self.enable_delete = tk.BooleanVar(value=True) self.enable_combined_sort = tk.BooleanVar(value=True) self.enable_combined_dedupe = tk.BooleanVar(value=True) def setup_logging(self): """配置日志记录""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('csv_processor.log', encoding='utf-8'), logging.StreamHandler() ] ) self.logger = logging.getLogger(__name__) self.logger.info("===== 程序启动 =====") def create_widgets(self): """创建所有界面组件""" # 主容器 main_container = ttk.Frame(self.root, padding=5) main_container.pack(fill=tk.BOTH, expand=True) # 使用notebook分页组织功能 self.notebook = ttk.Notebook(main_container) self.notebook.pack(fill=tk.BOTH, expand=True) # 创建各个标签页 self.create_file_tab() self.create_process_tab() self.create_delete_tab() self.create_merge_tab() self.create_combined_tab() # 新增组合处理标签页 self.create_log_tab() def create_file_tab(self): """创建文件操作标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="文件操作") # 文件选择部分 frame = ttk.LabelFrame(tab, text="CSV文件选择", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) ttk.Label(frame, text="文件路径:").grid(row=0, column=0, sticky=tk.W) ttk.Entry(frame, textvariable=self.file_path, width=40).grid(row=0, column=1, sticky=tk.EW) ttk.Button(frame, text="浏览", command=self.select_file).grid(row=0, column=2, padx=5) # 表头行选择 ttk.Label(frame, text="表头选择:").grid(row=1, column=0, sticky=tk.W) self.header_row_combobox = ttk.Combobox( frame, textvariable=self.header_row_index, state="readonly", width=5, style="Header.TCombobox" ) self.header_row_combobox.grid(row=1, column=1, sticky=tk.W) ttk.Label(frame, text="(0表示第一行)").grid(row=1, column=2, sticky=tk.W) # 重新解析按钮 ttk.Button(frame, text="重新解析", command=self.reparse_data, style="Accent.TButton").grid(row=1, column=3, padx=5) # 文件信息显示 self.file_info = scrolledtext.ScrolledText(tab, height=8, width=80) self.file_info.pack(fill=tk.X, padx=5, pady=5) def create_process_tab(self): """创建数据处理标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="排序/去重") # 排序选项部分 frame = ttk.LabelFrame(tab, text="排序选项", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) ttk.Checkbutton(frame, text="启用排序", variable=self.enable_sort, command=self.toggle_sort).grid(row=0, column=0, sticky=tk.W) ttk.Label(frame, text="排序表头:").grid(row=1, column=0, sticky=tk.W) self.sort_header_combobox = ttk.Combobox(frame, textvariable=self.sort_header, state="readonly") self.sort_header_combobox.grid(row=1, column=1, sticky=tk.EW) ttk.Label(frame, text="排序方式:").grid(row=2, column=0, sticky=tk.W) self.sort_order_combobox = ttk.Combobox( frame, textvariable=self.sort_order, values=["升序", "降序", "自定义字母排序"] ) self.sort_order_combobox.grid(row=2, column=1, sticky=tk.W) # 自定义字母排序范围 ttk.Checkbutton(frame, text="启用字母范围过滤", variable=self.enable_custom_letter_sort, command=self.toggle_letter_sort).grid(row=3, column=0, sticky=tk.W) ttk.Label(frame, text="字母范围:").grid(row=4, column=0, sticky=tk.W) self.letter_range_start_entry = ttk.Entry(frame, textvariable=self.letter_range_start, width=5) self.letter_range_start_entry.grid(row=4, column=1, sticky=tk.W) ttk.Label(frame, text="到").grid(row=4, column=2) self.letter_range_end_entry = ttk.Entry(frame, textvariable=self.letter_range_end, width=5) self.letter_range_end_entry.grid(row=4, column=3, sticky=tk.W) # 去重选项部分 frame = ttk.LabelFrame(tab, text="去重选项", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) ttk.Checkbutton(frame, text="启用去重", variable=self.enable_dedupe, command=self.toggle_dedupe).grid(row=0, column=0, sticky=tk.W) ttk.Label(frame, text="去重表头:").grid(row=1, column=0, sticky=tk.W) self.dedupe_header_combobox = ttk.Combobox(frame, textvariable=self.dedupe_header, state="readonly") self.dedupe_header_combobox.grid(row=1, column=1, sticky=tk.EW) # 处理按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="处理并保存到桌面", command=self.process_csv, style="Accent.TButton").pack() def create_delete_tab(self): """创建删除行标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="删除行") frame = ttk.LabelFrame(tab, text="删除包含指定字符的行", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) # 删除条件设置 ttk.Label(frame, text="搜索列:").grid(row=0, column=0, sticky=tk.W) self.delete_column_combobox = ttk.Combobox(frame, textvariable=self.delete_column, state="readonly") self.delete_column_combobox.grid(row=0, column=1, sticky=tk.EW) ttk.Label(frame, text="关键字:").grid(row=1, column=0, sticky=tk.W) ttk.Entry(frame, textvariable=self.delete_keyword).grid(row=1, column=1, sticky=tk.EW) ttk.Checkbutton(frame, text="区分大小写", variable=self.delete_case_sensitive).grid(row=2, column=0, sticky=tk.W) # 执行按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="执行删除并保存到桌面", command=self.delete_rows_with_keyword, style="Accent.TButton").pack() def create_merge_tab(self): """创建文件合并标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="文件合并") # 合并文件部分 frame = ttk.LabelFrame(tab, text="合并CSV文件", padding=10) frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 文件列表容器 list_frame = ttk.Frame(frame) list_frame.pack(fill=tk.BOTH, expand=True) ttk.Label(list_frame, text="已选择文件:").grid(row=0, column=0, sticky=tk.W) # 文件列表和滚动条 self.merge_file_canvas = tk.Canvas(list_frame, height=150) self.merge_file_canvas.grid(row=1, column=0, sticky=tk.EW) scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.merge_file_canvas.yview) scrollbar.grid(row=1, column=1, sticky=tk.NS) self.merge_file_canvas.configure(yscrollcommand=scrollbar.set) self.merge_file_frame = ttk.Frame(self.merge_file_canvas) self.merge_file_canvas.create_window((0, 0), window=self.merge_file_frame, anchor="nw") # 按钮区域 btn_frame = ttk.Frame(frame) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="添加文件", command=self.add_merge_file).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="清空列表", command=self.clear_merge_list).pack(side=tk.LEFT, padx=5) # 合并选项 opt_frame = ttk.Frame(frame) opt_frame.pack(fill=tk.X, pady=5) ttk.Label(opt_frame, text="合并依据列(可选):").grid(row=0, column=0, sticky=tk.W) self.merge_column_combo = ttk.Combobox(opt_frame, textvariable=self.merge_column, state="readonly") self.merge_column_combo.grid(row=0, column=1, sticky=tk.EW) # 合并按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="执行合并并保存到桌面", command=self.merge_csv_files, style="Accent.TButton").pack() def create_combined_tab(self): """创建组合处理标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="组合处理") # 组合处理选项 frame = ttk.LabelFrame(tab, text="组合处理选项", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) # 删除行选项 delete_frame = ttk.Frame(frame) delete_frame.pack(fill=tk.X, pady=5) ttk.Checkbutton(delete_frame, text="启用删除行", variable=self.enable_delete).pack(side=tk.LEFT, padx=5) ttk.Label(delete_frame, text="搜索列:").pack(side=tk.LEFT, padx=5) self.combined_delete_column_combobox = ttk.Combobox( delete_frame, textvariable=self.delete_column, state="readonly", width=15 ) self.combined_delete_column_combobox.pack(side=tk.LEFT, padx=5) ttk.Label(delete_frame, text="关键字:").pack(side=tk.LEFT, padx=5) ttk.Entry(delete_frame, textvariable=self.delete_keyword, width=15).pack(side=tk.LEFT, padx=5) ttk.Checkbutton(delete_frame, text="区分大小写", variable=self.delete_case_sensitive).pack(side=tk.LEFT, padx=5) # 排序选项 sort_frame = ttk.Frame(frame) sort_frame.pack(fill=tk.X, pady=5) ttk.Checkbutton(sort_frame, text="启用排序", variable=self.enable_combined_sort).pack(side=tk.LEFT, padx=5) ttk.Label(sort_frame, text="排序表头:").pack(side=tk.LEFT, padx=5) self.combined_sort_header_combobox = ttk.Combobox( sort_frame, textvariable=self.sort_header, state="readonly", width=15 ) self.combined_sort_header_combobox.pack(side=tk.LEFT, padx=5) ttk.Label(sort_frame, text="排序方式:").pack(side=tk.LEFT, padx=5) self.combined_sort_order_combobox = ttk.Combobox( sort_frame, textvariable=self.sort_order, values=["升序", "降序", "自定义字母排序"], width=15 ) self.combined_sort_order_combobox.pack(side=tk.LEFT, padx=5) # 自定义字母排序范围(新增) ttk.Checkbutton( sort_frame, text="启用字母范围", variable=self.enable_custom_letter_sort ).pack(side=tk.LEFT, padx=5) ttk.Label(sort_frame, text="从").pack(side=tk.LEFT, padx=5) ttk.Entry(sort_frame, textvariable=self.letter_range_start, width=3).pack(side=tk.LEFT) ttk.Label(sort_frame, text="到").pack(side=tk.LEFT, padx=5) ttk.Entry(sort_frame, textvariable=self.letter_range_end, width=3).pack(side=tk.LEFT) # 去重选项 dedupe_frame = ttk.Frame(frame) dedupe_frame.pack(fill=tk.X, pady=5) ttk.Checkbutton(dedupe_frame, text="启用去重", variable=self.enable_combined_dedupe).pack(side=tk.LEFT, padx=5) ttk.Label(dedupe_frame, text="去重表头:").pack(side=tk.LEFT, padx=5) self.combined_dedupe_header_combobox = ttk.Combobox( dedupe_frame, textvariable=self.dedupe_header, state="readonly", width=15 ) self.combined_dedupe_header_combobox.pack(side=tk.LEFT, padx=5) # 处理按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="执行组合处理并保存到桌面", command=self.combined_process, style="Accent.TButton").pack() def create_log_tab(self): """创建日志标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="运行日志") self.log_text = scrolledtext.ScrolledText(tab, height=15, width=80) self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) def log_message(self, message, level="info"): """记录日志并显示在GUI中""" log_methods = { "info": self.logger.info, "error": self.logger.error, "warning": self.logger.warning } # 记录到日志文件 log_methods.get(level, self.logger.info)(message) # 显示在GUI日志标签页 timestamp = datetime.now().strftime("%H:%M:%S") tagged_msg = f"[{timestamp}] {message}" self.log_text.insert(tk.END, tagged_msg + "\n") self.log_text.see(tk.END) # 同时在文件信息标签页显示重要信息 if level in ["error", "warning"]: self.file_info.config(state=tk.NORMAL) self.file_info.insert(tk.END, tagged_msg + "\n") self.file_info.config(state=tk.DISABLED) self.file_info.see(tk.END) def select_file(self): """选择CSV文件""" file_path = filedialog.askopenfilename( title="选择CSV文件", filetypes=[("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")] ) if file_path: self.file_path.set(file_path) self.log_message(f"已选择文件: {file_path}") self.load_csv(file_path) def reparse_data(self): """重新解析数据(使用新的表头行)""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return self.log_message(f"重新解析数据,使用表头行: {self.header_row_index.get()}") self.parse_data(self.raw_data) def load_csv(self, file_path): """加载CSV文件内容""" try: with open(file_path, 'r', encoding='utf-8-sig') as file: reader = csv.reader(file) self.raw_data = list(reader) # 保存原始数据 self.parse_data(self.raw_data) self.log_message(f"文件加载成功,共 {len(self.csv_data)} 行") except Exception as e: error_msg = f"读取CSV文件失败: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) def parse_data(self, raw_data): """解析原始数据,根据选择的表头行""" if not raw_data: return # 更新表头行选择框 row_options = list(range(len(raw_data))) self.header_row_combobox['values'] = row_options # 使用用户选择的表头行 header_index = self.header_row_index.get() if header_index < 0 or header_index >= len(raw_data): header_index = 0 self.header_row_index.set(0) # 设置表头和数据 # 表头行之前的数据保留为数据行 self.headers = raw_data[header_index] self.csv_data = raw_data[:header_index] + raw_data[header_index+1:] # 更新UI self.update_ui_with_headers() self.show_file_info(self.file_path.get()) def show_file_info(self, file_path): """显示文件信息""" self.file_info.config(state=tk.NORMAL) self.file_info.delete(1.0, tk.END) info = [ f"文件路径: {file_path}", f"总行数: {len(self.csv_data)}", f"列数: {len(self.headers)}", f"表头: {', '.join(self.headers)}", f"表头行: {self.header_row_index.get()}", "="*40, "前5行数据预览:" ] self.file_info.insert(tk.END, "\n".join(info) + "\n") # 显示前5行数据 for i, row in enumerate(self.csv_data[:5], 1): self.file_info.insert(tk.END, f"{i}. {', '.join(row)}\n") self.file_info.config(state=tk.DISABLED) def update_ui_with_headers(self): """根据加载的CSV更新UI元素""" # 更新所有下拉框 for combo in [ self.sort_header_combobox, self.dedupe_header_combobox, self.delete_column_combobox, self.merge_column_combo, self.combined_delete_column_combobox, self.combined_sort_header_combobox, self.combined_dedupe_header_combobox ]: combo['values'] = self.headers # 设置默认值 if self.headers: self.sort_header.set(self.headers[0]) self.dedupe_header.set(self.headers[0]) self.delete_column.set(self.headers[0]) self.merge_column.set("") def toggle_sort(self): """切换排序功能的启用状态""" state = "normal" if self.enable_sort.get() else "disabled" self.sort_header_combobox['state'] = state self.sort_order_combobox['state'] = state self.toggle_letter_sort() self.log_message(f"排序功能 {'启用' if self.enable_sort.get() else '禁用'}") def toggle_dedupe(self): """切换去重功能的启用状态""" state = "normal" if self.enable_dedupe.get() else "disabled" self.dedupe_header_combobox['state'] = state self.log_message(f"去重功能 {'启用' if self.enable_dedupe.get() else '禁用'}") def toggle_letter_sort(self): """控制字母范围输入框的启用状态""" if not self.enable_sort.get(): return state = "normal" if self.enable_custom_letter_sort.get() else "disabled" self.letter_range_start_entry['state'] = state self.letter_range_end_entry['state'] = state self.log_message(f"字母范围过滤 {'启用' if self.enable_custom_letter_sort.get() else '禁用'}") def add_merge_file(self): """添加要合并的文件""" file_paths = filedialog.askopenfilenames( title="选择要合并的CSV文件", filetypes=[("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")] ) if file_paths: for path in file_paths: if path not in self.merge_file_paths: self.merge_file_paths.append(path) self.update_merge_file_list() def clear_merge_list(self): """清空合并文件列表""" if self.merge_file_paths: self.merge_file_paths = [] self.update_merge_file_list() self.log_message("已清空合并文件列表") def update_merge_file_list(self): """更新合并文件列表显示""" # 清除现有内容 for widget in self.merge_file_frame.winfo_children(): widget.destroy() if not self.merge_file_paths: ttk.Label(self.merge_file_frame, text="尚未选择任何文件").pack() self.merge_file_canvas.configure(scrollregion=self.merge_file_canvas.bbox("all")) return # 添加文件列表 for i, path in enumerate(self.merge_file_paths): row_frame = ttk.Frame(self.merge_file_frame) row_frame.pack(fill=tk.X, pady=2) ttk.Label(row_frame, text=f"{i+1}. {os.path.basename(path)}", width=40, anchor="w").pack(side=tk.LEFT) ttk.Button(row_frame, text="移除", command=lambda p=path: self.remove_merge_file(p), style="Remove.TButton").pack(side=tk.LEFT, padx=2) # 更新滚动区域 self.merge_file_frame.update_idletasks() self.merge_file_canvas.configure(scrollregion=self.merge_file_canvas.bbox("all")) def remove_merge_file(self, file_path): """移除指定的合并文件""" if file_path in self.merge_file_paths: self.merge_file_paths.remove(file_path) self.update_merge_file_list() self.log_message(f"已移除文件: {file_path}") def delete_rows(self, data, column, keyword, case_sensitive): """删除包含关键字的行(通用方法)""" if not column or not keyword or not data: return data try: col_index = self.headers.index(column) if not case_sensitive: keyword = keyword.lower() new_data = [data[0]] # 保留表头 deleted_count = 0 for row in data[1:]: if len(row) > col_index: value = row[col_index] compare_value = value if case_sensitive else value.lower() if keyword not in compare_value: new_data.append(row) else: deleted_count += 1 self.log_message(f"删除行: 移除了 {deleted_count} 行包含 '{keyword}' 的数据") return new_data except Exception as e: error_msg = f"删除行时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) return data def sort_data(self, data, header, order, enable_letter_sort, letter_start, letter_end): """对数据进行排序(通用方法)""" if not header or not data: return data try: sort_index = self.headers.index(header) reverse = (order == "降序") # 字母范围过滤 if enable_letter_sort: try: letter_start = letter_start.upper() letter_end = letter_end.upper() if not (len(letter_start) == 1 and len(letter_end) == 1 and letter_start.isalpha() and letter_end.isalpha()): raise ValueError("字母范围必须是单个字母(如A-Z)") filtered_rows = [] for row in data[1:]: # 跳过表头 if len(row) > sort_index: value = str(row[sort_index]).strip().upper() if value and letter_start <= value[0] <= letter_end: filtered_rows.append(row) data = [data[0]] + filtered_rows self.log_message(f"字母范围过滤完成:{letter_start} 到 {letter_end}") except Exception as e: self.log_message(f"字母范围过滤失败: {str(e)}", "error") messagebox.showerror("错误", f"字母范围过滤失败: {str(e)}") return data # 排序逻辑 def sort_key(row): if len(row) > sort_index: value = row[sort_index] # 尝试解析为日期 for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%m/%d/%Y", "%Y.%m.%d"): try: return datetime.strptime(value, fmt) except ValueError: continue # 尝试解析为数字 try: return float(value) except ValueError: pass return value.lower() # 默认按字符串排序 return "" # 执行排序 if order == "自定义字母排序": data[1:] = sorted( data[1:], key=lambda x: str(sort_key(x)).lower() if len(x) > sort_index else "", reverse=False ) else: data[1:] = sorted(data[1:], key=sort_key, reverse=reverse) self.log_message(f"排序完成,表头 '{header}',顺序: {order}") return data except Exception as e: self.log_message(f"排序时出错: {str(e)}", "error") messagebox.showerror("错误", f"排序时出错: {str(e)}") return data def dedupe_data(self, data, header): """对数据进行去重(通用方法)""" if not header or not data: return data try: dedupe_index = self.headers.index(header) seen = set() unique_rows = [data[0]] # 保留表头 for row in data[1:]: if len(row) > dedupe_index: key = row[dedupe_index] if key not in seen: seen.add(key) unique_rows.append(row) self.log_message( f"去重完成,根据表头 '{header}' 删除重复项," f"原始行数: {len(data)},去重后行数: {len(unique_rows)}" ) return unique_rows except Exception as e: self.log_message(f"去重时出错: {str(e)}", "error") messagebox.showerror("错误", f"去重时出错: {str(e)}") return data def delete_rows_with_keyword(self): """删除包含关键字的行并保存到桌面""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return column = self.delete_column.get() keyword = self.delete_keyword.get() if not column: messagebox.showwarning("警告", "请选择要搜索的列") return if not keyword: messagebox.showwarning("警告", "请输入要搜索的关键字") return try: # 执行删除 processed_data = self.delete_rows( self.csv_data, column, keyword, self.delete_case_sensitive.get() ) # 生成保存路径 operation = f"deleted_{keyword}" save_path = self.generate_filename(self.file_path.get(), operation) # 保存文件 if self.save_csv_file(processed_data, save_path): # 更新当前数据 self.csv_data = processed_data messagebox.showinfo("成功", f"结果已保存到桌面:\n{os.path.basename(save_path)}") except Exception as e: error_msg = f"删除行时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) def get_desktop_path(self): """获取桌面路径""" try: desktop = os.path.join(os.path.join(os.environ['USERPROFILE']), 'Desktop') if os.path.exists(desktop): return desktop except KeyError: pass # 如果上面的方法失败,尝试其他方法 desktop = os.path.join(os.path.expanduser('~'), 'Desktop') if os.path.exists(desktop): return desktop # 如果还是失败,返回当前目录 return os.getcwd() def generate_filename(self, original_name, operation): """生成新的文件名""" if not original_name: original_name = "processed" base = os.path.basename(original_name) name, ext = os.path.splitext(base) # 清理操作名称中的特殊字符 clean_op = "".join(c if c.isalnum() else "_" for c in operation) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") new_name = f"{name}_{clean_op}_{timestamp}{ext}" return os.path.join(self.get_desktop_path(), new_name) def save_csv_file(self, data, save_path): """保存CSV文件到指定路径""" try: with open(save_path, 'w', encoding='utf-8-sig', newline='') as file: writer = csv.writer(file) writer.writerows(data) # 更新文件信息显示 self.show_file_info(save_path) self.log_message(f"文件已保存到: {save_path}") return True except Exception as e: error_msg = f"保存文件时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) return False def process_csv(self): """处理CSV文件(排序、去重等)并保存到桌面""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return if not self.csv_data: messagebox.showwarning("警告", "CSV文件没有数据") return self.log_message("开始处理CSV文件...") processed_data = self.csv_data.copy() # 去重处理 if self.enable_dedupe.get(): processed_data = self.dedupe_data( processed_data, self.dedupe_header.get() ) # 排序处理 if self.enable_sort.get(): processed_data = self.sort_data( processed_data, self.sort_header.get(), self.sort_order.get(), self.enable_custom_letter_sort.get(), self.letter_range_start.get(), self.letter_range_end.get() ) # 生成操作描述 operations = [] if self.enable_sort.get(): operations.append(f"sorted_{self.sort_header.get()}_{self.sort_order.get()}") if self.enable_dedupe.get(): operations.append(f"deduped_{self.dedupe_header.get()}") operation = "_".join(operations) if operations else "processed" # 生成保存路径 save_path = self.generate_filename(self.file_path.get(), operation) # 保存文件 if self.save_csv_file(processed_data, save_path): # 更新当前数据 self.csv_data = processed_data messagebox.showinfo("成功", f"文件处理完成,已保存到桌面:\n{os.path.basename(save_path)}") def combined_process(self): """组合处理:删除行 -> 排序 -> 去重""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return if not self.csv_data: messagebox.showwarning("警告", "CSV文件没有数据") return self.log_message("开始组合处理CSV文件...") processed_data = self.csv_data.copy() operations = [] # 1. 删除行 if self.enable_delete.get(): column = self.delete_column.get() keyword = self.delete_keyword.get() if column and keyword: processed_data = self.delete_rows( processed_data, column, keyword, self.delete_case_sensitive.get() ) operations.append(f"deleted_{keyword}") # 2. 排序 if self.enable_combined_sort.get(): header = self.sort_header.get() order = self.sort_order.get() if header: processed_data = self.sort_data( processed_data, header, order, self.enable_custom_letter_sort.get(), self.letter_range_start.get(), self.letter_range_end.get() ) operations.append(f"sorted_{header}_{order}") # 3. 去重 if self.enable_combined_dedupe.get(): header = self.dedupe_header.get() if header: processed_data = self.dedupe_data( processed_data, header ) operations.append(f"deduped_{header}") # 生成操作描述 operation = "combined_" + "_".join(operations) if operations else "combined_processed" # 生成保存路径 save_path = self.generate_filename(self.file_path.get(), operation) # 保存文件 if self.save_csv_file(processed_data, save_path): # 更新当前数据 self.csv_data = processed_data messagebox.showinfo("成功", f"组合处理完成,已保存到桌面:\n{os.path.basename(save_path)}") def merge_csv_files(self): """合并多个CSV文件并保存到桌面""" if not self.merge_file_paths: messagebox.showwarning("警告", "请先添加要合并的文件") return try: # 检查所有文件是否存在 missing_files = [f for f in self.merge_file_paths if not os.path.exists(f)] if missing_files: raise FileNotFoundError(f"以下文件不存在: {', '.join(missing_files)}") merge_column = self.merge_column.get() common_headers = None all_data = [] # 收集所有文件的表头和数据 header_sets = [] for file_path in self.merge_file_paths: with open(file_path, 'r', encoding='utf-8-sig') as file: reader = csv.reader(file) data = list(reader) if data: header_sets.append(set(data[0])) all_data.append(data) # 找出共同表头 if header_sets: common_headers = set(header_sets[0]) for headers in header_sets[1:]: common_headers.intersection_update(headers) common_headers = sorted(common_headers) if not common_headers: raise ValueError("选中的文件没有共同的列,无法合并") # 如果没有指定合并依据列,使用所有共同列 merge_indices = None if merge_column: if merge_column not in common_headers: raise ValueError(f"合并依据列 '{merge_column}' 不在共同列中") merge_indices = [i for i, h in enumerate(common_headers) if h == merge_column] # 合并数据 merged_data = [common_headers.copy()] key_counter = defaultdict(int) for data in all_data: if not data: continue headers = data[0] header_map = {h: i for i, h in enumerate(headers)} for row in data[1:]: # 如果指定了合并列,检查是否已存在相同键 if merge_indices: merge_values = [row[header_map[h]] for h in common_headers if h == merge_column] if merge_values: key = tuple(merge_values) key_counter[key] += 1 if key_counter[key] > 1: continue # 跳过重复键的行 # 构建新行,只保留共同列 new_row = [] for col in common_headers: if col in header_map and len(row) > header_map[col]: new_row.append(row[header_map[col]]) else: new_row.append("") merged_data.append(new_row) # 生成操作描述 operation = "merged" if merge_column: operation += f"_by_{merge_column}" # 生成保存路径 first_file = os.path.basename(self.merge_file_paths[0]) save_path = self.generate_filename(first_file, operation) # 保存文件 if self.save_csv_file(merged_data, save_path): messagebox.showinfo("成功", f"文件合并完成,已保存到桌面:\n{os.path.basename(save_path)}") except Exception as e: error_msg = f"合并文件时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) if __name__ == "__main__": root = tk.Tk() app = CSVProcessorApp(root) root.mainloop() 优化这个代码, 按钮: 正常状态:浅蓝色背景(#4a90e2) + 浅黄色边框(#ffcc00) + 白色加粗文字 悬停状态:稍深的蓝色背景(#3a80d2) 所有操作按钮(浏览、处理、保存等)都使用此样式 字体: 全局使用11号Arial字体(原始为9号) 按钮文字加粗 日志区域同样增大字体 布局: 增加所有组件之间的间距,使界面更宽松 标签页内边距增加,减少拥挤感 按钮宽度增加,更易点击 颜色方案: 主色调:浅蓝色按钮 + 浅黄色边框 辅助色:白色文字 + 浅灰色背景
07-10
import tkinter as tk from tkinter import filedialog, messagebox, ttk, scrolledtext import csv from datetime import datetime import logging import os from collections import defaultdict class CSVProcessorApp: def init(self, root): self.root = root self.root.title(“CSV_ProcessPro”) self.root.geometry(“800x600”) self.root.resizable(False, False) # 初始化变量 self.file_path = tk.StringVar() self.csv_data = [] self.headers = [] self.raw_data = [] # 存储原始数据 self.header_row_index = tk.IntVar(value=0) # 表头行索引 self.setup_variables() self.setup_logging() self.create_widgets() self.setup_styles() def setup_styles(self): """设置全局样式""" self.style = ttk.Style() self.style.configure("TFrame", background="#f0f0f0") self.style.configure("TLabel", background="#f0f0f0", font=('Arial', 9)) self.style.configure("TButton", font=('Arial', 9, 'bold')) self.style.configure("Accent.TButton", foreground="black", font=('Arial', 9, 'bold'), borderwidth=2, relief="raised") self.style.map("Accent.TButton", background=[("active", "#4a90e2"), ("!active", "#d4e6ff")], bordercolor=[("active", "#4a90e2"), ("!active", "#ffcc00")]) self.style.configure("Remove.TButton", foreground="black", font=('Arial', 8), background="#ffcccc", borderwidth=1, relief="solid") self.style.map("Remove.TButton", background=[("active", "#ff9999"), ("!active", "#ffcccc")]) self.style.configure("Header.TCombobox", font=('Arial', 9)) def setup_variables(self): """初始化所有动态变量""" # 排序相关 self.sort_header = tk.StringVar() self.sort_order = tk.StringVar(value="升序") # 去重相关 self.dedupe_header = tk.StringVar() # 删除行相关 self.delete_keyword = tk.StringVar() self.delete_column = tk.StringVar() self.delete_case_sensitive = tk.BooleanVar() # 合并文件相关 self.merge_file_paths = [] self.merge_column = tk.StringVar() # 状态变量 self.enable_sort = tk.BooleanVar() self.enable_dedupe = tk.BooleanVar() self.enable_custom_letter_sort = tk.BooleanVar() self.letter_range_start = tk.StringVar(value="A") self.letter_range_end = tk.StringVar(value="Z") # 组合处理相关 self.enable_delete = tk.BooleanVar(value=True) self.enable_combined_sort = tk.BooleanVar(value=True) self.enable_combined_dedupe = tk.BooleanVar(value=True) def setup_logging(self): """配置日志记录""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('csv_processor.log', encoding='utf-8'), logging.StreamHandler() ] ) self.logger = logging.getLogger(__name__) self.logger.info("===== 程序启动 =====") def create_widgets(self): """创建所有界面组件""" # 主容器 main_container = ttk.Frame(self.root, padding=5) main_container.pack(fill=tk.BOTH, expand=True) # 使用notebook分页组织功能 self.notebook = ttk.Notebook(main_container) self.notebook.pack(fill=tk.BOTH, expand=True) # 创建各个标签页 self.create_file_tab() self.create_process_tab() self.create_delete_tab() self.create_merge_tab() self.create_combined_tab() # 新增组合处理标签页 self.create_log_tab() def create_file_tab(self): """创建文件操作标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="文件操作") # 文件选择部分 frame = ttk.LabelFrame(tab, text="CSV文件选择", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) ttk.Label(frame, text="文件路径:").grid(row=0, column=0, sticky=tk.W) ttk.Entry(frame, textvariable=self.file_path, width=40).grid(row=0, column=1, sticky=tk.EW) ttk.Button(frame, text="浏览", command=self.select_file).grid(row=0, column=2, padx=5) # 表头行选择 ttk.Label(frame, text="表头选择:").grid(row=1, column=0, sticky=tk.W) self.header_row_combobox = ttk.Combobox( frame, textvariable=self.header_row_index, state="readonly", width=5, style="Header.TCombobox" ) self.header_row_combobox.grid(row=1, column=1, sticky=tk.W) ttk.Label(frame, text="(0表示第一行)").grid(row=1, column=2, sticky=tk.W) # 重新解析按钮 ttk.Button(frame, text="重新解析", command=self.reparse_data, style="Accent.TButton").grid(row=1, column=3, padx=5) # 文件信息显示 self.file_info = scrolledtext.ScrolledText(tab, height=8, width=80) self.file_info.pack(fill=tk.X, padx=5, pady=5) def create_process_tab(self): """创建数据处理标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="排序/去重") # 排序选项部分 frame = ttk.LabelFrame(tab, text="排序选项", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) ttk.Checkbutton(frame, text="启用排序", variable=self.enable_sort, command=self.toggle_sort).grid(row=0, column=0, sticky=tk.W) ttk.Label(frame, text="排序表头:").grid(row=1, column=0, sticky=tk.W) self.sort_header_combobox = ttk.Combobox(frame, textvariable=self.sort_header, state="readonly") self.sort_header_combobox.grid(row=1, column=1, sticky=tk.EW) ttk.Label(frame, text="排序方式:").grid(row=2, column=0, sticky=tk.W) self.sort_order_combobox = ttk.Combobox( frame, textvariable=self.sort_order, values=["升序", "降序", "自定义字母排序"] ) self.sort_order_combobox.grid(row=2, column=1, sticky=tk.W) # 自定义字母排序范围 ttk.Checkbutton(frame, text="启用字母范围过滤", variable=self.enable_custom_letter_sort, command=self.toggle_letter_sort).grid(row=3, column=0, sticky=tk.W) ttk.Label(frame, text="字母范围:").grid(row=4, column=0, sticky=tk.W) self.letter_range_start_entry = ttk.Entry(frame, textvariable=self.letter_range_start, width=5) self.letter_range_start_entry.grid(row=4, column=1, sticky=tk.W) ttk.Label(frame, text="到").grid(row=4, column=2) self.letter_range_end_entry = ttk.Entry(frame, textvariable=self.letter_range_end, width=5) self.letter_range_end_entry.grid(row=4, column=3, sticky=tk.W) # 去重选项部分 frame = ttk.LabelFrame(tab, text="去重选项", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) ttk.Checkbutton(frame, text="启用去重", variable=self.enable_dedupe, command=self.toggle_dedupe).grid(row=0, column=0, sticky=tk.W) ttk.Label(frame, text="去重表头:").grid(row=1, column=0, sticky=tk.W) self.dedupe_header_combobox = ttk.Combobox(frame, textvariable=self.dedupe_header, state="readonly") self.dedupe_header_combobox.grid(row=1, column=1, sticky=tk.EW) # 处理按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="处理并保存到桌面", command=self.process_csv, style="Accent.TButton").pack() def create_delete_tab(self): """创建删除行标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="删除行") frame = ttk.LabelFrame(tab, text="删除包含指定字符的行", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) # 删除条件设置 ttk.Label(frame, text="搜索列:").grid(row=0, column=0, sticky=tk.W) self.delete_column_combobox = ttk.Combobox(frame, textvariable=self.delete_column, state="readonly") self.delete_column_combobox.grid(row=0, column=1, sticky=tk.EW) ttk.Label(frame, text="关键字:").grid(row=1, column=0, sticky=tk.W) ttk.Entry(frame, textvariable=self.delete_keyword).grid(row=1, column=1, sticky=tk.EW) ttk.Checkbutton(frame, text="区分大小写", variable=self.delete_case_sensitive).grid(row=2, column=0, sticky=tk.W) # 执行按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="执行删除并保存到桌面", command=self.delete_rows_with_keyword, style="Accent.TButton").pack() def create_merge_tab(self): """创建文件合并标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="文件合并") # 合并文件部分 frame = ttk.LabelFrame(tab, text="合并CSV文件", padding=10) frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 文件列表容器 list_frame = ttk.Frame(frame) list_frame.pack(fill=tk.BOTH, expand=True) ttk.Label(list_frame, text="已选择文件:").grid(row=0, column=0, sticky=tk.W) # 文件列表和滚动条 self.merge_file_canvas = tk.Canvas(list_frame, height=150) self.merge_file_canvas.grid(row=1, column=0, sticky=tk.EW) scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.merge_file_canvas.yview) scrollbar.grid(row=1, column=1, sticky=tk.NS) self.merge_file_canvas.configure(yscrollcommand=scrollbar.set) self.merge_file_frame = ttk.Frame(self.merge_file_canvas) self.merge_file_canvas.create_window((0, 0), window=self.merge_file_frame, anchor="nw") # 按钮区域 btn_frame = ttk.Frame(frame) btn_frame.pack(fill=tk.X, pady=5) ttk.Button(btn_frame, text="添加文件", command=self.add_merge_file).pack(side=tk.LEFT, padx=5) ttk.Button(btn_frame, text="清空列表", command=self.clear_merge_list).pack(side=tk.LEFT, padx=5) # 合并选项 opt_frame = ttk.Frame(frame) opt_frame.pack(fill=tk.X, pady=5) ttk.Label(opt_frame, text="合并依据列(可选):").grid(row=0, column=0, sticky=tk.W) self.merge_column_combo = ttk.Combobox(opt_frame, textvariable=self.merge_column, state="readonly") self.merge_column_combo.grid(row=0, column=1, sticky=tk.EW) # 合并按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="执行合并并保存到桌面", command=self.merge_csv_files, style="Accent.TButton").pack() def create_combined_tab(self): """创建组合处理标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="组合处理") # 组合处理选项 frame = ttk.LabelFrame(tab, text="组合处理选项", padding=10) frame.pack(fill=tk.X, padx=5, pady=5) # 删除行选项 delete_frame = ttk.Frame(frame) delete_frame.pack(fill=tk.X, pady=5) ttk.Checkbutton(delete_frame, text="启用删除行", variable=self.enable_delete).pack(side=tk.LEFT, padx=5) ttk.Label(delete_frame, text="搜索列:").pack(side=tk.LEFT, padx=5) self.combined_delete_column_combobox = ttk.Combobox( delete_frame, textvariable=self.delete_column, state="readonly", width=15 ) self.combined_delete_column_combobox.pack(side=tk.LEFT, padx=5) ttk.Label(delete_frame, text="关键字:").pack(side=tk.LEFT, padx=5) ttk.Entry(delete_frame, textvariable=self.delete_keyword, width=15).pack(side=tk.LEFT, padx=5) ttk.Checkbutton(delete_frame, text="区分大小写", variable=self.delete_case_sensitive).pack(side=tk.LEFT, padx=5) # 排序选项 sort_frame = ttk.Frame(frame) sort_frame.pack(fill=tk.X, pady=5) ttk.Checkbutton(sort_frame, text="启用排序", variable=self.enable_combined_sort).pack(side=tk.LEFT, padx=5) ttk.Label(sort_frame, text="排序表头:").pack(side=tk.LEFT, padx=5) self.combined_sort_header_combobox = ttk.Combobox( sort_frame, textvariable=self.sort_header, state="readonly", width=15 ) self.combined_sort_header_combobox.pack(side=tk.LEFT, padx=5) ttk.Label(sort_frame, text="排序方式:").pack(side=tk.LEFT, padx=5) self.combined_sort_order_combobox = ttk.Combobox( sort_frame, textvariable=self.sort_order, values=["升序", "降序", "自定义字母排序"], width=15 ) self.combined_sort_order_combobox.pack(side=tk.LEFT, padx=5) # 自定义字母排序范围(新增) ttk.Checkbutton( sort_frame, text="启用字母范围", variable=self.enable_custom_letter_sort ).pack(side=tk.LEFT, padx=5) ttk.Label(sort_frame, text="从").pack(side=tk.LEFT, padx=5) ttk.Entry(sort_frame, textvariable=self.letter_range_start, width=3).pack(side=tk.LEFT) ttk.Label(sort_frame, text="到").pack(side=tk.LEFT, padx=5) ttk.Entry(sort_frame, textvariable=self.letter_range_end, width=3).pack(side=tk.LEFT) # 去重选项 dedupe_frame = ttk.Frame(frame) dedupe_frame.pack(fill=tk.X, pady=5) ttk.Checkbutton(dedupe_frame, text="启用去重", variable=self.enable_combined_dedupe).pack(side=tk.LEFT, padx=5) ttk.Label(dedupe_frame, text="去重表头:").pack(side=tk.LEFT, padx=5) self.combined_dedupe_header_combobox = ttk.Combobox( dedupe_frame, textvariable=self.dedupe_header, state="readonly", width=15 ) self.combined_dedupe_header_combobox.pack(side=tk.LEFT, padx=5) # 处理按钮 btn_frame = ttk.Frame(tab) btn_frame.pack(pady=10) ttk.Button(btn_frame, text="执行组合处理并保存到桌面", command=self.combined_process, style="Accent.TButton").pack() def create_log_tab(self): """创建日志标签页""" tab = ttk.Frame(self.notebook) self.notebook.add(tab, text="运行日志") self.log_text = scrolledtext.ScrolledText(tab, height=15, width=80) self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) def log_message(self, message, level="info"): """记录日志并显示在GUI中""" log_methods = { "info": self.logger.info, "error": self.logger.error, "warning": self.logger.warning } # 记录到日志文件 log_methods.get(level, self.logger.info)(message) # 显示在GUI日志标签页 timestamp = datetime.now().strftime("%H:%M:%S") tagged_msg = f"[{timestamp}] {message}" self.log_text.insert(tk.END, tagged_msg + "\n") self.log_text.see(tk.END) # 同时在文件信息标签页显示重要信息 if level in ["error", "warning"]: self.file_info.config(state=tk.NORMAL) self.file_info.insert(tk.END, tagged_msg + "\n") self.file_info.config(state=tk.DISABLED) self.file_info.see(tk.END) def select_file(self): """选择CSV文件""" file_path = filedialog.askopenfilename( title="选择CSV文件", filetypes=[("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")] ) if file_path: self.file_path.set(file_path) self.log_message(f"已选择文件: {file_path}") self.load_csv(file_path) def reparse_data(self): """重新解析数据(使用新的表头行)""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return self.log_message(f"重新解析数据,使用表头行: {self.header_row_index.get()}") self.parse_data(self.raw_data) def load_csv(self, file_path): """加载CSV文件内容""" try: with open(file_path, 'r', encoding='utf-8-sig') as file: reader = csv.reader(file) self.raw_data = list(reader) # 保存原始数据 self.parse_data(self.raw_data) self.log_message(f"文件加载成功,共 {len(self.csv_data)} 行") except Exception as e: error_msg = f"读取CSV文件失败: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) def parse_data(self, raw_data): """解析原始数据,根据选择的表头行""" if not raw_data: return # 更新表头行选择框 row_options = list(range(len(raw_data))) self.header_row_combobox['values'] = row_options # 使用用户选择的表头行 header_index = self.header_row_index.get() if header_index < 0 or header_index >= len(raw_data): header_index = 0 self.header_row_index.set(0) # 设置表头和数据 # 表头行之前的数据保留为数据行 self.headers = raw_data[header_index] self.csv_data = raw_data[:header_index] + raw_data[header_index+1:] # 更新UI self.update_ui_with_headers() self.show_file_info(self.file_path.get()) def show_file_info(self, file_path): """显示文件信息""" self.file_info.config(state=tk.NORMAL) self.file_info.delete(1.0, tk.END) info = [ f"文件路径: {file_path}", f"总行数: {len(self.csv_data)}", f"列数: {len(self.headers)}", f"表头: {', '.join(self.headers)}", f"表头行: {self.header_row_index.get()}", "="*40, "前5行数据预览:" ] self.file_info.insert(tk.END, "\n".join(info) + "\n") # 显示前5行数据 for i, row in enumerate(self.csv_data[:5], 1): self.file_info.insert(tk.END, f"{i}. {', '.join(row)}\n") self.file_info.config(state=tk.DISABLED) def update_ui_with_headers(self): """根据加载的CSV更新UI元素""" # 更新所有下拉框 for combo in [ self.sort_header_combobox, self.dedupe_header_combobox, self.delete_column_combobox, self.merge_column_combo, self.combined_delete_column_combobox, self.combined_sort_header_combobox, self.combined_dedupe_header_combobox ]: combo['values'] = self.headers # 设置默认值 if self.headers: self.sort_header.set(self.headers[0]) self.dedupe_header.set(self.headers[0]) self.delete_column.set(self.headers[0]) self.merge_column.set("") def toggle_sort(self): """切换排序功能的启用状态""" state = "normal" if self.enable_sort.get() else "disabled" self.sort_header_combobox['state'] = state self.sort_order_combobox['state'] = state self.toggle_letter_sort() self.log_message(f"排序功能 {'启用' if self.enable_sort.get() else '禁用'}") def toggle_dedupe(self): """切换去重功能的启用状态""" state = "normal" if self.enable_dedupe.get() else "disabled" self.dedupe_header_combobox['state'] = state self.log_message(f"去重功能 {'启用' if self.enable_dedupe.get() else '禁用'}") def toggle_letter_sort(self): """控制字母范围输入框的启用状态""" if not self.enable_sort.get(): return state = "normal" if self.enable_custom_letter_sort.get() else "disabled" self.letter_range_start_entry['state'] = state self.letter_range_end_entry['state'] = state self.log_message(f"字母范围过滤 {'启用' if self.enable_custom_letter_sort.get() else '禁用'}") def add_merge_file(self): """添加要合并的文件""" file_paths = filedialog.askopenfilenames( title="选择要合并的CSV文件", filetypes=[("CSV文件", "*.csv"), ("文本文件", "*.txt"), ("所有文件", "*.*")] ) if file_paths: for path in file_paths: if path not in self.merge_file_paths: self.merge_file_paths.append(path) self.update_merge_file_list() def clear_merge_list(self): """清空合并文件列表""" if self.merge_file_paths: self.merge_file_paths = [] self.update_merge_file_list() self.log_message("已清空合并文件列表") def update_merge_file_list(self): """更新合并文件列表显示""" # 清除现有内容 for widget in self.merge_file_frame.winfo_children(): widget.destroy() if not self.merge_file_paths: ttk.Label(self.merge_file_frame, text="尚未选择任何文件").pack() self.merge_file_canvas.configure(scrollregion=self.merge_file_canvas.bbox("all")) return # 添加文件列表 for i, path in enumerate(self.merge_file_paths): row_frame = ttk.Frame(self.merge_file_frame) row_frame.pack(fill=tk.X, pady=2) ttk.Label(row_frame, text=f"{i+1}. {os.path.basename(path)}", width=40, anchor="w").pack(side=tk.LEFT) ttk.Button(row_frame, text="移除", command=lambda p=path: self.remove_merge_file(p), style="Remove.TButton").pack(side=tk.LEFT, padx=2) # 更新滚动区域 self.merge_file_frame.update_idletasks() self.merge_file_canvas.configure(scrollregion=self.merge_file_canvas.bbox("all")) def remove_merge_file(self, file_path): """移除指定的合并文件""" if file_path in self.merge_file_paths: self.merge_file_paths.remove(file_path) self.update_merge_file_list() self.log_message(f"已移除文件: {file_path}") def delete_rows(self, data, column, keyword, case_sensitive): """删除包含关键字的行(通用方法)""" if not column or not keyword or not data: return data try: col_index = self.headers.index(column) if not case_sensitive: keyword = keyword.lower() new_data = [data[0]] # 保留表头 deleted_count = 0 for row in data[1:]: if len(row) > col_index: value = row[col_index] compare_value = value if case_sensitive else value.lower() if keyword not in compare_value: new_data.append(row) else: deleted_count += 1 self.log_message(f"删除行: 移除了 {deleted_count} 行包含 '{keyword}' 的数据") return new_data except Exception as e: error_msg = f"删除行时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) return data def sort_data(self, data, header, order, enable_letter_sort, letter_start, letter_end): """对数据进行排序(通用方法)""" if not header or not data: return data try: sort_index = self.headers.index(header) reverse = (order == "降序") # 字母范围过滤 if enable_letter_sort: try: letter_start = letter_start.upper() letter_end = letter_end.upper() if not (len(letter_start) == 1 and len(letter_end) == 1 and letter_start.isalpha() and letter_end.isalpha()): raise ValueError("字母范围必须是单个字母(如A-Z)") filtered_rows = [] for row in data[1:]: # 跳过表头 if len(row) > sort_index: value = str(row[sort_index]).strip().upper() if value and letter_start <= value[0] <= letter_end: filtered_rows.append(row) data = [data[0]] + filtered_rows self.log_message(f"字母范围过滤完成:{letter_start} 到 {letter_end}") except Exception as e: self.log_message(f"字母范围过滤失败: {str(e)}", "error") messagebox.showerror("错误", f"字母范围过滤失败: {str(e)}") return data # 排序逻辑 def sort_key(row): if len(row) > sort_index: value = row[sort_index] # 尝试解析为日期 for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d", "%m/%d/%Y", "%Y.%m.%d"): try: return datetime.strptime(value, fmt) except ValueError: continue # 尝试解析为数字 try: return float(value) except ValueError: pass return value.lower() # 默认按字符串排序 return "" # 执行排序 if order == "自定义字母排序": data[1:] = sorted( data[1:], key=lambda x: str(sort_key(x)).lower() if len(x) > sort_index else "", reverse=False ) else: data[1:] = sorted(data[1:], key=sort_key, reverse=reverse) self.log_message(f"排序完成,表头 '{header}',顺序: {order}") return data except Exception as e: self.log_message(f"排序时出错: {str(e)}", "error") messagebox.showerror("错误", f"排序时出错: {str(e)}") return data def dedupe_data(self, data, header): """对数据进行去重(通用方法)""" if not header or not data: return data try: dedupe_index = self.headers.index(header) seen = set() unique_rows = [data[0]] # 保留表头 for row in data[1:]: if len(row) > dedupe_index: key = row[dedupe_index] if key not in seen: seen.add(key) unique_rows.append(row) self.log_message( f"去重完成,根据表头 '{header}' 删除重复项," f"原始行数: {len(data)},去重后行数: {len(unique_rows)}" ) return unique_rows except Exception as e: self.log_message(f"去重时出错: {str(e)}", "error") messagebox.showerror("错误", f"去重时出错: {str(e)}") return data def delete_rows_with_keyword(self): """删除包含关键字的行并保存到桌面""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return column = self.delete_column.get() keyword = self.delete_keyword.get() if not column: messagebox.showwarning("警告", "请选择要搜索的列") return if not keyword: messagebox.showwarning("警告", "请输入要搜索的关键字") return try: # 执行删除 processed_data = self.delete_rows( self.csv_data, column, keyword, self.delete_case_sensitive.get() ) # 生成保存路径 operation = f"deleted_{keyword}" save_path = self.generate_filename(self.file_path.get(), operation) # 保存文件 if self.save_csv_file(processed_data, save_path): # 更新当前数据 self.csv_data = processed_data messagebox.showinfo("成功", f"结果已保存到桌面:\n{os.path.basename(save_path)}") except Exception as e: error_msg = f"删除行时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) def get_desktop_path(self): """获取桌面路径""" try: desktop = os.path.join(os.path.join(os.environ['USERPROFILE']), 'Desktop') if os.path.exists(desktop): return desktop except KeyError: pass # 如果上面的方法失败,尝试其他方法 desktop = os.path.join(os.path.expanduser('~'), 'Desktop') if os.path.exists(desktop): return desktop # 如果还是失败,返回当前目录 return os.getcwd() def generate_filename(self, original_name, operation): """生成新的文件名""" if not original_name: original_name = "processed" base = os.path.basename(original_name) name, ext = os.path.splitext(base) # 清理操作名称中的特殊字符 clean_op = "".join(c if c.isalnum() else "_" for c in operation) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") new_name = f"{name}_{clean_op}_{timestamp}{ext}" return os.path.join(self.get_desktop_path(), new_name) def save_csv_file(self, data, save_path): """保存CSV文件到指定路径""" try: with open(save_path, 'w', encoding='utf-8-sig', newline='') as file: writer = csv.writer(file) writer.writerows(data) # 更新文件信息显示 self.show_file_info(save_path) self.log_message(f"文件已保存到: {save_path}") return True except Exception as e: error_msg = f"保存文件时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) return False def process_csv(self): """处理CSV文件(排序、去重等)并保存到桌面""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return if not self.csv_data: messagebox.showwarning("警告", "CSV文件没有数据") return self.log_message("开始处理CSV文件...") processed_data = self.csv_data.copy() # 去重处理 if self.enable_dedupe.get(): processed_data = self.dedupe_data( processed_data, self.dedupe_header.get() ) # 排序处理 if self.enable_sort.get(): processed_data = self.sort_data( processed_data, self.sort_header.get(), self.sort_order.get(), self.enable_custom_letter_sort.get(), self.letter_range_start.get(), self.letter_range_end.get() ) # 生成操作描述 operations = [] if self.enable_sort.get(): operations.append(f"sorted_{self.sort_header.get()}_{self.sort_order.get()}") if self.enable_dedupe.get(): operations.append(f"deduped_{self.dedupe_header.get()}") operation = "_".join(operations) if operations else "processed" # 生成保存路径 save_path = self.generate_filename(self.file_path.get(), operation) # 保存文件 if self.save_csv_file(processed_data, save_path): # 更新当前数据 self.csv_data = processed_data messagebox.showinfo("成功", f"文件处理完成,已保存到桌面:\n{os.path.basename(save_path)}") def combined_process(self): """组合处理:删除行 -> 排序 -> 去重""" if not self.file_path.get(): messagebox.showwarning("警告", "请先选择CSV文件") return if not self.csv_data: messagebox.showwarning("警告", "CSV文件没有数据") return self.log_message("开始组合处理CSV文件...") processed_data = self.csv_data.copy() operations = [] # 1. 删除行 if self.enable_delete.get(): column = self.delete_column.get() keyword = self.delete_keyword.get() if column and keyword: processed_data = self.delete_rows( processed_data, column, keyword, self.delete_case_sensitive.get() ) operations.append(f"deleted_{keyword}") # 2. 排序 if self.enable_combined_sort.get(): header = self.sort_header.get() order = self.sort_order.get() if header: processed_data = self.sort_data( processed_data, header, order, self.enable_custom_letter_sort.get(), self.letter_range_start.get(), self.letter_range_end.get() ) operations.append(f"sorted_{header}_{order}") # 3. 去重 if self.enable_combined_dedupe.get(): header = self.dedupe_header.get() if header: processed_data = self.dedupe_data( processed_data, header ) operations.append(f"deduped_{header}") # 生成操作描述 operation = "combined_" + "_".join(operations) if operations else "combined_processed" # 生成保存路径 save_path = self.generate_filename(self.file_path.get(), operation) # 保存文件 if self.save_csv_file(processed_data, save_path): # 更新当前数据 self.csv_data = processed_data messagebox.showinfo("成功", f"组合处理完成,已保存到桌面:\n{os.path.basename(save_path)}") def merge_csv_files(self): """合并多个CSV文件并保存到桌面""" if not self.merge_file_paths: messagebox.showwarning("警告", "请先添加要合并的文件") return try: # 检查所有文件是否存在 missing_files = [f for f in self.merge_file_paths if not os.path.exists(f)] if missing_files: raise FileNotFoundError(f"以下文件不存在: {', '.join(missing_files)}") merge_column = self.merge_column.get() common_headers = None all_data = [] # 收集所有文件的表头和数据 header_sets = [] for file_path in self.merge_file_paths: with open(file_path, 'r', encoding='utf-8-sig') as file: reader = csv.reader(file) data = list(reader) if data: header_sets.append(set(data[0])) all_data.append(data) # 找出共同表头 if header_sets: common_headers = set(header_sets[0]) for headers in header_sets[1:]: common_headers.intersection_update(headers) common_headers = sorted(common_headers) if not common_headers: raise ValueError("选中的文件没有共同的列,无法合并") # 如果没有指定合并依据列,使用所有共同列 merge_indices = None if merge_column: if merge_column not in common_headers: raise ValueError(f"合并依据列 '{merge_column}' 不在共同列中") merge_indices = [i for i, h in enumerate(common_headers) if h == merge_column] # 合并数据 merged_data = [common_headers.copy()] key_counter = defaultdict(int) for data in all_data: if not data: continue headers = data[0] header_map = {h: i for i, h in enumerate(headers)} for row in data[1:]: # 如果指定了合并列,检查是否已存在相同键 if merge_indices: merge_values = [row[header_map[h]] for h in common_headers if h == merge_column] if merge_values: key = tuple(merge_values) key_counter[key] += 1 if key_counter[key] > 1: continue # 跳过重复键的行 # 构建新行,只保留共同列 new_row = [] for col in common_headers: if col in header_map and len(row) > header_map[col]: new_row.append(row[header_map[col]]) else: new_row.append("") merged_data.append(new_row) # 生成操作描述 operation = "merged" if merge_column: operation += f"_by_{merge_column}" # 生成保存路径 first_file = os.path.basename(self.merge_file_paths[0]) save_path = self.generate_filename(first_file, operation) # 保存文件 if self.save_csv_file(merged_data, save_path): messagebox.showinfo("成功", f"文件合并完成,已保存到桌面:\n{os.path.basename(save_path)}") except Exception as e: error_msg = f"合并文件时出错: {str(e)}" self.log_message(error_msg, "error") messagebox.showerror("错误", error_msg) if name == “main”: root = tk.Tk() app = CSVProcessorApp(root) root.mainloop() 优化这个代码, 按钮: 正常状态:浅蓝色背景(#4a90e2) + 浅黄色边框(#ffcc00) + 白色加粗文字 悬停状态:稍深的蓝色背景(#3a80d2) 所有操作按钮(浏览、处理、保存等)都使用此样式 字体: 全局使用11号Arial字体(原始为9号) 按钮文字加粗 日志区域同样增大字体 布局: 增加所有组件之间的间距,使界面更宽松 标签页内边距增加,减少拥挤感 按钮宽度增加,更易点击 颜色方案: 主色调:浅蓝色按钮 + 浅黄色边框 辅助色:白色文字 + 浅灰色背景, 输出优化后的完整代码
最新发布
07-10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值