AS3自定义滚动条需要注意的一点.非常容易出错哦.

本文介绍了一种在AS3中优化自定义滚动条的方法。通过调整事件监听方式,解决了拖动过程中易中断的问题,并提供了代码示例。

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

AS3自定义滚动条需要注意的一点.非常容易出错哦.非常重要.

 

一个影片剪辑

通过添加鼠标按下,鼠标弹起,鼠标移动的事件监听来做 滚动条,

为什么移动过快,或者拖动过程中会中断啊...给一个边界的限制的时候,拖动过快也会拖出边界~ 郁闷哇~

以前直接在按钮上添加代码onPress什么的时候貌似没这问题。  是这样的情况吧?

 

如果你的代码是这样:

 

private function sliderBarMouseDownHander(evt:MouseEvent):void {
                     _sliderBarMoving=true;
                     _sliderBarMouseX=evt.localX;
              }
              private function sliderBarMouseUpHander(evt:MouseEvent):void {
                     _sliderBarMoving=false;
              }
              private function sliderBarMouseMoveHander(evt:MouseEvent):void {
                     if (_sliderBarMoving) {
                            _sliderBar.x+=evt.localX-_sliderBarMouseX;
                     }
              }

效果很不好,很容易拖着拖着就松开sliderBar了,

 

 

解决办法:

这几个事件的用法如下:

首先初始化的时候给被拖动的物体假设实例名是 thumb 添加事件监听

     thumb.addEventListener( MouseEvent.MOUSE_DOWN, onMouseDown );
     thumb.addEventListener( MouseEvent.MOUSE_UP,onMouseUp );

其次是事件处理函数



  private function onMouseDown( e : MouseEvent ) : void
  {
   stage.addEventListener( MouseEvent.MOUSE_MOVE, onMoveHandler );
   stage.addEventListener( MouseEvent.MOUSE_UP, onMouseUp );
  }
  
  private function onMouseUp( e : MouseEvent ) : void
  {
   if( stage.hasEventListener( MouseEvent.MOUSE_MOVE ) )
   {
    stage.removeEventListener( MouseEvent.MOUSE_MOVE, onMoveHandler );
   }
   if( stage.hasEventListener( MouseEvent.MOUSE_UP ) )
   {
    stage.removeEventListener( MouseEvent.MOUSE_UP, onMouseUp );
   }
  }

  private function onMoveHandler( e : MouseEvent ) : void
  {
   thumb.x = mouseX;
   thumb.y = mouseY;
  }
..............
为何给stage 添加MOUSE_MOVE 事件,用了明了

出处 博客园 http://www.cnblogs.com/Mirage/archive/2008/08/27/1277473.html?updated=1

感谢:sprite115

 

 

来自:http://bbs.blueidea.com/viewthread.php?tid=2882156&pid=4183948&page=1&extra=page%3D1#pid4183948

7楼有源文件,去拿吧.

 

将以下代码做成PDF教案,详述每段代码的作用,包括整个程序的架构 import tkinter as tk from tkinter import filedialog, messagebox from tkinter import ttk import pandas as pd import json import os import sys class DataAnalysisApp: def __init__(self, root): self.root = root self.root.title("数据分析助手") # 配置文件路径 self.config_file = self.get_resource_path("app_config.json") # 加载配置 self.config = self.load_config() # 设置窗口大小和位置 window_width = self.config.get("window_width", 1000) window_height = self.config.get("window_height", 600) screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() center_x = int(screen_width/2 - window_width/2) center_y = int(screen_height/2 - window_height/2) self.root.geometry(f&#39;{window_width}x{window_height}+{center_x}+{center_y}&#39;) # 设置窗口最小尺寸 self.root.minsize(800, 400) # 创建菜单栏 self.menu_bar = tk.Menu(self.root) # 文件菜单 self.file_menu = tk.Menu(self.menu_bar, tearoff=0) self.file_menu.add_command(label="打开", command=self.open_file) self.file_menu.add_separator() self.file_menu.add_command(label="退出", command=self.root.quit) self.menu_bar.add_cascade(label="文件", menu=self.file_menu) # 添加公式菜单 self.formula_menu = tk.Menu(self.menu_bar, tearoff=0) self.formula_menu.add_command(label="自定义公式", command=self.open_formula_window) self.menu_bar.add_cascade(label="公式", menu=self.formula_menu) self.root.config(menu=self.menu_bar) # 创建主框架 self.main_frame = ttk.Frame(self.root) self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # 创建可拖拽的分隔窗口 self.paned_window = ttk.PanedWindow(self.main_frame, orient=tk.HORIZONTAL) self.paned_window.pack(fill=tk.BOTH, expand=True) # 创建左侧数据显示区域 self.left_frame = ttk.Frame(self.paned_window) # 创建右侧控制面板 self.right_frame = ttk.Frame(self.paned_window) # 添加框架到分隔窗口 self.paned_window.add(self.left_frame, weight=1) self.paned_window.add(self.right_frame, weight=0) # 设置分隔位置 if "paned_position" in self.config: self.paned_window.after(100, lambda: self.paned_window.sashpos(0, self.config["paned_position"])) # 创建算法选择区域 self.algorithm_frame = ttk.LabelFrame(self.right_frame, text="算法选择", padding=10) self.algorithm_frame.pack(fill=tk.X, pady=(0, 10)) # 添加算法选择下拉框 self.algorithm_var = tk.StringVar() self.algorithms = [ "描述性统计", "相关性分析", "数据分布分析", "时间序列分析", "分组统计分析", "缺失值分析", "CPK分析" ] self.algorithm_combo = ttk.Combobox( self.algorithm_frame, textvariable=self.algorithm_var, values=self.algorithms, state="readonly" ) self.algorithm_combo.pack(fill=tk.X, pady=(5, 0)) self.algorithm_combo.set("请选择分析方法") # 添加运行按钮 self.run_button = ttk.Button( self.algorithm_frame, text="运行分析", command=self.run_analysis ) self.run_button.pack(fill=tk.X, pady=(10, 0)) # 创建结果显示区域 self.result_frame = ttk.LabelFrame(self.right_frame, text="分析结果", padding=10) self.result_frame.pack(fill=tk.BOTH, expand=True) # 添加结果文本框 self.result_text = tk.Text( self.result_frame, wrap=tk.WORD, width=30, height=20, font=(&#39;Arial&#39;, 10) # 设置字体 ) # 为结果文本框添加滚动条 self.result_scrollbar = ttk.Scrollbar( self.result_frame, orient="vertical", command=self.result_text.yview ) # 正确放置滚动条和文本框 self.result_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 配置文本框的滚动 self.result_text.configure(yscrollcommand=self.result_scrollbar.set) # 配置文本标签样式 self.result_text.tag_configure(&#39;header&#39;, font=(&#39;Arial&#39;, 11, &#39;bold&#39;)) self.result_text.tag_configure(&#39;subtitle&#39;, font=(&#39;Arial&#39;, 10, &#39;bold&#39;)) self.result_text.tag_configure(&#39;warning&#39;, foreground=&#39;orange&#39;) self.result_text.tag_configure(&#39;error&#39;, foreground=&#39;red&#39;) # 设置为只读 self.result_text.config(state=&#39;disabled&#39;) # 创建框架来容纳Treeview和滚动条 self.tree_frame = ttk.Frame(self.left_frame) self.tree_frame.pack(fill=tk.BOTH, expand=True) # 创建并配置Treeview样式 style = ttk.Style() style.configure("Treeview", rowheight=22, # 稍微减小行高 font=(&#39;Arial&#39;, 9), # 更改字体大小 background="#FFFFFF", fieldbackground="#FFFFFF", foreground="#000000", borderwidth=1, relief=&#39;solid&#39; ) # 配置标题样式,更接近Excel style.configure("Treeview.Heading", font=(&#39;Arial&#39;, 9, &#39;bold&#39;), relief=&#39;flat&#39;, borderwidth=1, background=&#39;#F0F0F0&#39;, # Excel风格的标题背景色 foreground=&#39;#000000&#39; ) # 设置选中颜色为Excel风格的蓝色 style.map(&#39;Treeview&#39;, background=[(&#39;selected&#39;, &#39;#E1E9F5&#39;)], # Excel选中的浅蓝色 foreground=[(&#39;selected&#39;, &#39;#000000&#39;)] # 选中时保持黑色文字 ) # 设置Treeview网格线颜色 style.configure("Treeview", background="white", fieldbackground="white", foreground="black", bordercolor="#DDD", # 网格线颜色 lightcolor="#DDD", # 亮边框颜色 darkcolor="#DDD" # 暗边框颜色 ) # 创建Treeview控件用于显示数据 self.tree = ttk.Treeview(self.tree_frame) # 创建垂直滚动条 self.vsb = ttk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview) self.vsb.pack(side=tk.RIGHT, fill=tk.Y) # 创建水平滚动条 self.hsb = ttk.Scrollbar(self.tree_frame, orient="horizontal", command=self.tree.xview) self.hsb.pack(side=tk.BOTTOM, fill=tk.X) # 设置Treeview的滚动 self.tree.configure(yscrollcommand=self.vsb.set, xscrollcommand=self.hsb.set) # 放置Treeview self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 显示行标题 self.tree["show"] = "headings" # 创建状态栏 self.status_bar = ttk.Label(self.root, text="就绪", anchor=tk.W) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=3) # 添加数据存储变量 self.current_data = None def run_analysis(self): if self.current_data is None: messagebox.showwarning("警告", "请先加载数据") return selected_algorithm = self.algorithm_var.get() if selected_algorithm == "请选择分析方法": messagebox.showwarning("警告", "请选择分析方法") return try: # 创建不包含前两列的数据副本 analysis_data = self.current_data.iloc[:, 2:].copy() if analysis_data.empty: messagebox.showwarning("警告", "没有可分析的数据列") return self.result_text.config(state=&#39;normal&#39;) self.result_text.delete(1.0, tk.END) if selected_algorithm == "CPK分析": self._run_cpk_analysis(analysis_data) elif selected_algorithm == "描述性统计": self._run_descriptive_analysis(analysis_data) elif selected_algorithm == "相关性分析": self._run_correlation_analysis(analysis_data) elif selected_algorithm == "数据分布分析": self._run_distribution_analysis(analysis_data) elif selected_algorithm == "时间序列分析": self._run_time_series_analysis(analysis_data) elif selected_algorithm == "分组统计分析": self._run_group_analysis(analysis_data) elif selected_algorithm == "缺失值分析": self._run_missing_value_analysis(analysis_data) self.result_text.config(state=&#39;disabled&#39;) except Exception as e: self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, f"⚠ 分析过程出错:\n{str(e)}", &#39;error&#39;) self.result_text.config(state=&#39;disabled&#39;) def _run_descriptive_analysis(self, data): """描述性统计""" numeric_cols = data.select_dtypes(include=[&#39;int64&#39;, &#39;float64&#39;]).columns non_numeric_cols = data.select_dtypes(exclude=[&#39;int64&#39;, &#39;float64&#39;]).columns # 处理数值列 if not numeric_cols.empty: numeric_stats = data[numeric_cols].describe() self.result_text.insert(tk.END, "═══ 数值型数据统计 ═══\n\n", &#39;header&#39;) # 格式化数值统计结果 for col in numeric_cols: stats = numeric_stats[col] self.result_text.insert(tk.END, f"▶ {col}\n", &#39;subtitle&#39;) self.result_text.insert(tk.END, f" • 数量: {stats[&#39;count&#39;]:.0f}\n") self.result_text.insert(tk.END, f" • 均值: {stats[&#39;mean&#39;]:.2f}\n") self.result_text.insert(tk.END, f" • 标准差: {stats[&#39;std&#39;]:.2f}\n") self.result_text.insert(tk.END, f" • 最小值: {stats[&#39;min&#39;]:.2f}\n") self.result_text.insert(tk.END, f" • 25%分位: {stats[&#39;25%&#39;]:.2f}\n") self.result_text.insert(tk.END, f" • 中位数: {stats[&#39;50%&#39;]:.2f}\n") self.result_text.insert(tk.END, f" • 75%分位: {stats[&#39;75%&#39;]:.2f}\n") self.result_text.insert(tk.END, f" • 最大值: {stats[&#39;max&#39;]:.2f}\n") self.result_text.insert(tk.END, "\n") # 处理非数值列 if not non_numeric_cols.empty: self.result_text.insert(tk.END, "═══ 非数值型数据统计 ═══\n\n", &#39;header&#39;) for col in non_numeric_cols: value_counts = data[col].value_counts() unique_count = data[col].nunique() total_count = len(data[col]) self.result_text.insert(tk.END, f"▶ {col}\n", &#39;subtitle&#39;) self.result_text.insert(tk.END, f" • 总数据量: {total_count}\n") self.result_text.insert(tk.END, f" • 唯一值数量: {unique_count}\n") self.result_text.insert(tk.END, " • 前5项频率分布:\n") # 显示前5个值的频率分布 for val, count in value_counts.head().items(): percentage = (count / total_count) * 100 self.result_text.insert(tk.END, f" - {val}: {count} ({percentage:.1f}%)\n") self.result_text.insert(tk.END, "\n") def _run_correlation_analysis(self, data): """相关性分析""" numeric_data = data.select_dtypes(include=[&#39;int64&#39;, &#39;float64&#39;]) if numeric_data.empty: self.result_text.insert(tk.END, "⚠ 没有找到可以进行相关性分析的数值型数据", &#39;warning&#39;) else: result = numeric_data.corr() self.result_text.insert(tk.END, "═══ 相关性分析结果 ═══\n\n", &#39;header&#39;) # 格式化相关性矩阵 for col1 in result.columns: self.result_text.insert(tk.END, f"▶ {col1} 的相关性:\n", &#39;subtitle&#39;) for col2 in result.columns: if col1 != col2: # 不显示自身的相关性 corr = result.loc[col1, col2] # 添加相关性强度的描述 strength = "" if abs(corr) > 0.7: strength = "强" elif abs(corr) > 0.4: strength = "中等" else: strength = "弱" self.result_text.insert(tk.END, f" • 与 {col2}: {corr:.3f} ({strength}相关)\n") self.result_text.insert(tk.END, "\n") def _run_distribution_analysis(self, data): """数据分布分析""" numeric_cols = data.select_dtypes(include=[&#39;int64&#39;, &#39;float64&#39;]).columns if numeric_cols.empty: self.result_text.insert(tk.END, "⚠ 没有找到可以分析的数值型数据", &#39;warning&#39;) return self.result_text.insert(tk.END, "═══ 数据分布分析 ═══\n\n", &#39;header&#39;) for col in numeric_cols: # 修改变量名,避免与参数名冲突 col_data = data[col].dropna() # 计算分布相关指标 skewness = col_data.skew() kurtosis = col_data.kurtosis() # 计算分位数 quantiles = col_data.quantile([0.1, 0.25, 0.5, 0.75, 0.9]) self.result_text.insert(tk.END, f"▶ {col}\n", &#39;subtitle&#39;) self.result_text.insert(tk.END, f" • 偏度: {skewness:.3f}\n") self.result_text.insert(tk.END, f" • 峰度: {kurtosis:.3f}\n") self.result_text.insert(tk.END, " • 分位数分布:\n") self.result_text.insert(tk.END, f" - 10%: {quantiles[0.1]:.2f}\n") self.result_text.insert(tk.END, f" - 25%: {quantiles[0.25]:.2f}\n") self.result_text.insert(tk.END, f" - 50%: {quantiles[0.5]:.2f}\n") self.result_text.insert(tk.END, f" - 75%: {quantiles[0.75]:.2f}\n") self.result_text.insert(tk.END, f" - 90%: {quantiles[0.9]:.2f}\n\n") def _run_time_series_analysis(self, data): """时间序列分析""" # 查找日期列 date_cols = data.select_dtypes(include=[&#39;datetime64&#39;]).columns if date_cols.empty: self.result_text.insert(tk.END, "⚠ 没有找到日期类型的列\n", &#39;warning&#39;) return self.result_text.insert(tk.END, "═══ 时间序列分析 ═══\n\n", &#39;header&#39;) for date_col in date_cols: self.result_text.insert(tk.END, f"▶ {date_col} 时间分布\n", &#39;subtitle&#39;) # 基本时间范围 time_min = data[date_col].min() time_max = data[date_col].max() time_range = time_max - time_min self.result_text.insert(tk.END, f" • 时间范围: {time_range.days} 天\n") self.result_text.insert(tk.END, f" • 起始时间: {time_min:%Y-%m-%d}\n") self.result_text.insert(tk.END, f" • 结束时间: {time_max:%Y-%m-%d}\n\n") # 按月份分布 monthly_counts = data[date_col].dt.month.value_counts().sort_index() self.result_text.insert(tk.END, " • 月份分布:\n") for month, count in monthly_counts.items(): self.result_text.insert(tk.END, f" - {month}月: {count}条记录\n") self.result_text.insert(tk.END, "\n") def _run_group_analysis(self, data): """分组统计分析""" # 获取可能的分组列(分类数据) category_cols = data.select_dtypes(include=[&#39;object&#39;, &#39;category&#39;]).columns numeric_cols = data.select_dtypes(include=[&#39;int64&#39;, &#39;float64&#39;]).columns if category_cols.empty or numeric_cols.empty: self.result_text.insert(tk.END, "⚠ 需要同时包含分类数据和数值数据\n", &#39;warning&#39;) return self.result_text.insert(tk.END, "═══ 分组统计分析 ═══\n\n", &#39;header&#39;) for cat_col in category_cols: self.result_text.insert(tk.END, f"▶ 按 {cat_col} 分组统计\n", &#39;subtitle&#39;) # 计算每个分组的基本统计量 for num_col in numeric_cols: group_stats = data.groupby(cat_col)[num_col].agg([ &#39;count&#39;, &#39;mean&#39;, &#39;std&#39;, &#39;min&#39;, &#39;max&#39; ]) self.result_text.insert(tk.END, f" • {num_col} 统计:\n") for group_name, stats in group_stats.iterrows(): self.result_text.insert(tk.END, f" - {group_name}:\n") self.result_text.insert(tk.END, f" 数量: {stats[&#39;count&#39;]:.0f}\n") self.result_text.insert(tk.END, f" 均值: {stats[&#39;mean&#39;]:.2f}\n") self.result_text.insert(tk.END, f" 标准差: {stats[&#39;std&#39;]:.2f}\n") self.result_text.insert(tk.END, f" 最小值: {stats[&#39;min&#39;]:.2f}\n") self.result_text.insert(tk.END, f" 最大值: {stats[&#39;max&#39;]:.2f}\n") self.result_text.insert(tk.END, "\n") def _run_missing_value_analysis(self, data): """缺失值分析""" self.result_text.insert(tk.END, "═══ 缺失值分析 ═══\n\n", &#39;header&#39;) # 计算每列的缺失值 missing_stats = data.isnull().sum() total_rows = len(data) # 只显示有缺失值的列 missing_cols = missing_stats[missing_stats > 0] if missing_cols.empty: self.result_text.insert(tk.END, "✓ 数据中没有发现缺失值\n", &#39;subtitle&#39;) return self.result_text.insert(tk.END, "▶ 缺失值统计\n", &#39;subtitle&#39;) for col, missing_count in missing_cols.items(): missing_percentage = (missing_count / total_rows) * 100 self.result_text.insert(tk.END, f" • {col}:\n") self.result_text.insert(tk.END, f" - 缺失数量: {missing_count}\n") self.result_text.insert(tk.END, f" - 缺失比例: {missing_percentage:.2f}%\n") # 添加缺失值模式分析 self.result_text.insert(tk.END, "\n▶ 缺失值模式\n", &#39;subtitle&#39;) total_missing = data.isnull().sum().sum() self.result_text.insert(tk.END, f" • 总缺失值数量: {total_missing}\n") self.result_text.insert(tk.END, f" • 总缺失率: {(total_missing/(total_rows*len(data.columns))):.2f}%\n") def _run_cpk_analysis(self, data): """CPK分析""" numeric_cols = data.select_dtypes(include=[&#39;int64&#39;, &#39;float64&#39;]).columns if numeric_cols.empty: self.result_text.insert(tk.END, "⚠ 没有找到可以进行CPK分析的数值型数据", &#39;warning&#39;) return # 创建输入对话框获取规格限 spec_dialog = tk.Toplevel(self.root) spec_dialog.title("输入规格限") spec_dialog.geometry("400x500") # 增加窗口大小 # 使对话框成为模态窗口 spec_dialog.transient(self.root) spec_dialog.grab_set() # 创建主框架,并添加滚动条 main_frame = ttk.Frame(spec_dialog) main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建Canvas滚动条 canvas = tk.Canvas(main_frame) scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview) # 创建内容框架 content_frame = ttk.Frame(canvas) # 配置Canvas canvas.configure(yscrollcommand=scrollbar.set) # 创建规格限输入框 specs = {} row = 0 # 添加标题标签 title_label = ttk.Label(content_frame, text="请输入各列的规格上下限:", font=(&#39;Arial&#39;, 10, &#39;bold&#39;)) title_label.grid(row=row, column=0, columnspan=3, pady=10, padx=5, sticky=&#39;w&#39;) row += 1 for col in numeric_cols: # 列名标签 col_label = ttk.Label(content_frame, text=f"{col}:", font=(&#39;Arial&#39;, 9)) col_label.grid(row=row, column=0, pady=5, padx=5, sticky=&#39;w&#39;) # USL输入框和标签 usl_frame = ttk.Frame(content_frame) usl_frame.grid(row=row, column=1, padx=5, sticky=&#39;w&#39;) usl_var = tk.StringVar() ttk.Entry(usl_frame, textvariable=usl_var, width=12).pack(side=tk.LEFT, padx=2) ttk.Label(usl_frame, text="USL").pack(side=tk.LEFT, padx=2) row += 1 # LSL输入框和标签 lsl_frame = ttk.Frame(content_frame) lsl_frame.grid(row=row, column=1, padx=5, sticky=&#39;w&#39;) lsl_var = tk.StringVar() ttk.Entry(lsl_frame, textvariable=lsl_var, width=12).pack(side=tk.LEFT, padx=2) ttk.Label(lsl_frame, text="LSL").pack(side=tk.LEFT, padx=2) specs[col] = {&#39;usl&#39;: usl_var, &#39;lsl&#39;: lsl_var} row += 1 # 添加分隔线 ttk.Separator(content_frame, orient=&#39;horizontal&#39;).grid( row=row, column=0, columnspan=3, sticky=&#39;ew&#39;, pady=5) row += 1 # 添加按钮框架 button_frame = ttk.Frame(content_frame) button_frame.grid(row=row, column=0, columnspan=3, pady=10) ttk.Button(button_frame, text="计算CPK", command=lambda: calculate_cpk()).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="取消", command=spec_dialog.destroy).pack(side=tk.LEFT, padx=5) # 放置Canvas滚动条 canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 将content_frame放入canvas canvas_window = canvas.create_window((0, 0), window=content_frame, anchor=&#39;nw&#39;) # 配置canvas滚动区域 def configure_scroll_region(event): canvas.configure(scrollregion=canvas.bbox(&#39;all&#39;)) # 配置canvas宽度 def configure_canvas_width(event): canvas.itemconfig(canvas_window, width=event.width) # 绑定事件 content_frame.bind(&#39;<Configure>&#39;, configure_scroll_region) canvas.bind(&#39;<Configure>&#39;, configure_canvas_width) # 绑定鼠标滚轮 def on_mousewheel(event): canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") canvas.bind_all("<MouseWheel>", on_mousewheel) def calculate_cpk(): """计算CPK""" try: # 确保文本框可编辑 self.result_text.config(state=&#39;normal&#39;) self.result_text.delete(1.0, tk.END) for col in numeric_cols: try: # 获取规格限 usl = float(specs[col][&#39;usl&#39;].get()) lsl = float(specs[col][&#39;lsl&#39;].get()) # 获取数据 values = data[col].dropna() # 计算统计量 mean = values.mean() std = values.std() # 计算CPU和CPL cpu = (usl - mean) / (3 * std) cpl = (mean - lsl) / (3 * std) # 计算CPK cpk = min(cpu, cpl) # 计算过程能力评级 rating = "未知" if cpk >= 1.67: rating = "极佳" elif cpk >= 1.33: rating = "良好" elif cpk >= 1.0: rating = "合格" else: rating = "不合格" # 显示结果 self.result_text.insert(tk.END, f"▶ {col}\n", &#39;subtitle&#39;) self.result_text.insert(tk.END, f" • 均值: {mean:.3f}\n") self.result_text.insert(tk.END, f" • 标准差: {std:.3f}\n") self.result_text.insert(tk.END, f" • USL: {usl:.3f}\n") self.result_text.insert(tk.END, f" • LSL: {lsl:.3f}\n") self.result_text.insert(tk.END, f" • CPU: {cpu:.3f}\n") self.result_text.insert(tk.END, f" • CPL: {cpl:.3f}\n") self.result_text.insert(tk.END, f" • CPK: {cpk:.3f}\n") self.result_text.insert(tk.END, f" • 过程能力评级: {rating}\n\n") except ValueError: self.result_text.insert(tk.END, f"⚠ {col}: 输入数值无效\n", &#39;warning&#39;) except Exception as e: self.result_text.insert(tk.END, f"⚠ {col}: 计算出错 - {str(e)}\n", &#39;error&#39;) # 设置文本框为只读 self.result_text.config(state=&#39;disabled&#39;) # 关闭对话框 spec_dialog.destroy() except Exception as e: messagebox.showerror("错误", f"计算过程出错:{str(e)}") # 确保发生错误时也设置文本框为只读 self.result_text.config(state=&#39;disabled&#39;) def open_file(self): file_path = filedialog.askopenfilename( title="选择文件", filetypes=(("Excel files", "*.xlsx;*.xls"), ("All files", "*.*")) ) if file_path: try: # 使用pandas读取Excel数据 self.current_data = pd.read_excel(file_path) data = self.current_data # 清除现有的Treeview数据 self.tree.delete(*self.tree.get_children()) # 设置Treeview的列和标题 self.tree["columns"] = list(data.columns) for col in data.columns: # 更精确的列宽计算 max_width = max( len(str(col)) * 7, # 进一步减小系数 data[col].astype(str).str.len().max() * 7 ) width = min(max(max_width, 50), 150) # 更紧凑的列宽范围 self.tree.column(col, anchor=tk.W, width=width, minwidth=40, # 更小的最小宽度 stretch=True ) self.tree.heading(col, text=col, anchor=tk.W, ) # 插入数据到Treeview for i, (index, row) in enumerate(data.iterrows()): tags = (&#39;evenrow&#39;,) if i % 2 == 0 else (&#39;oddrow&#39;,) self.tree.insert("", "end", values=list(row), tags=tags) # 配置更细微的交替行颜色 self.tree.tag_configure(&#39;oddrow&#39;, background=&#39;#FAFAFA&#39;) # 更浅的灰色 self.tree.tag_configure(&#39;evenrow&#39;, background=&#39;#FFFFFF&#39;) # 纯白色 # 更新状态栏 self.status_bar.config( text=f"已加载 {len(data)} 行数据,{len(data.columns)} 列 | {file_path}" ) # 清除之前的分析结果 self.result_text.config(state=&#39;normal&#39;) self.result_text.delete(1.0, tk.END) self.result_text.config(state=&#39;disabled&#39;) self.algorithm_var.set("请选择分析方法") except Exception as e: messagebox.showerror("错误", f"无法读取文件: {e}") self.status_bar.config(text="读取文件失败") def load_config(self): """加载配置文件""" config_dir = os.path.expanduser("~/.data_analysis_app") self.config_file = os.path.join(config_dir, "config.json") # 确保配置目录存在 if not os.path.exists(config_dir): os.makedirs(config_dir) if os.path.exists(self.config_file): try: with open(self.config_file, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: return json.load(f) except: return {} return {} def save_config(self): """保存配置到文件""" config = { "window_width": self.root.winfo_width(), "window_height": self.root.winfo_height(), "paned_position": self.paned_window.sashpos(0) } try: with open(self.config_file, &#39;w&#39;, encoding=&#39;utf-8&#39;) as f: json.dump(config, f, indent=4) except Exception as e: print(f"保存配置失败: {e}") def on_sash_moved(self, event): """分隔条移动后的处理""" self.save_config() def on_closing(self): """窗口关闭时的处理""" self.save_config() self.root.destroy() def open_formula_window(self): """打开公式编辑窗口""" formula_window = tk.Toplevel(self.root) formula_window.title("自定义公式") formula_window.geometry("600x400") # 使窗口居中 window_width = 600 window_height = 400 screen_width = formula_window.winfo_screenwidth() screen_height = formula_window.winfo_screenheight() x = int((screen_width - window_width) / 2) y = int((screen_height - window_height) / 2) formula_window.geometry(f"{window_width}x{window_height}+{x}+{y}") # 创建主框架 main_frame = ttk.Frame(formula_window, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # 创建说明标签 ttk.Label(main_frame, text="在这里输入您的自定义公式:", font=(&#39;Arial&#39;, 10)).pack(anchor=tk.W) # 创建公式名称输入框 name_frame = ttk.Frame(main_frame) name_frame.pack(fill=tk.X, pady=(10,5)) ttk.Label(name_frame, text="公式名称:").pack(side=tk.LEFT) formula_name = ttk.Entry(name_frame) formula_name.pack(side=tk.LEFT, fill=tk.X, expand=True) # 创建公式输入区域 formula_frame = ttk.LabelFrame(main_frame, text="公式内容", padding="5") formula_frame.pack(fill=tk.BOTH, expand=True, pady=(5,10)) # 创建文本编辑器和滚动条的容器 text_container = ttk.Frame(formula_frame) text_container.pack(fill=tk.BOTH, expand=True) # 创建文本编辑器 formula_text = tk.Text(text_container, wrap=tk.WORD, font=(&#39;Consolas&#39;, 11)) # 创建垂直滚动条 v_scrollbar = ttk.Scrollbar(text_container, orient=tk.VERTICAL, command=formula_text.yview) v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 创建水平滚动条 h_scrollbar = ttk.Scrollbar(formula_frame, orient=tk.HORIZONTAL, command=formula_text.xview) h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) # 配置文本框的滚动 formula_text.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) formula_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 添加示例文本 example_text = """# 示例公式: # 可以使用 Python 语法编写公式 # 数据可通过 data 变量访问 def calculate(data): # 示例:计算某列的平均值 result = data[&#39;列名&#39;].mean() return result # 更多示例: # 1. 计算多列的平均值 # result = data[[&#39;列1&#39;, &#39;列2&#39;, &#39;列3&#39;]].mean() # 2. 条件筛选 # result = data[data[&#39;列名&#39;] > 100].mean() # 3. 自定义计算 # result = (data[&#39;列1&#39;] + data[&#39;列2&#39;]) / 2 # 4. 分组统计 # result = data.groupby(&#39;分组列&#39;)[&#39;值列&#39;].mean() # 5. 数据转换 # result = data[&#39;列名&#39;].apply(lambda x: x * 2) """ formula_text.insert(&#39;1.0&#39;, example_text) # 创建按钮框架 button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, pady=(0,5)) def save_formula(): """保存公式""" name = formula_name.get().strip() formula = formula_text.get(&#39;1.0&#39;, tk.END).strip() if not name: messagebox.showwarning("警告", "请输入公式名称") return if not formula: messagebox.showwarning("警告", "请输入公式内容") return try: # 保存公式到文件 formulas_file = "custom_formulas.json" formulas = {} # 读取现有公式 if os.path.exists(formulas_file): with open(formulas_file, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: formulas = json.load(f) # 添加或更新公式 formulas[name] = formula # 保存到文件 with open(formulas_file, &#39;w&#39;, encoding=&#39;utf-8&#39;) as f: json.dump(formulas, f, indent=4, ensure_ascii=False) messagebox.showinfo("成功", "公式保存成功!") formula_window.destroy() except Exception as e: messagebox.showerror("错误", f"保存公式失败:{str(e)}") def test_formula(): """测试公式""" if self.current_data is None: messagebox.showwarning("警告", "请先加载数据") return formula = formula_text.get(&#39;1.0&#39;, tk.END).strip() if not formula: messagebox.showwarning("警告", "请输入公式内容") return try: # 创建一个本地命名空间 local_dict = {} # 执行公式代码 exec(formula, globals(), local_dict) if &#39;calculate&#39; not in local_dict: raise ValueError("未找到 calculate 函数") # 执行计算 result = local_dict[&#39;calculate&#39;](self.current_data) # 显示结果 messagebox.showinfo("测试结果", f"计算结果:{result}") except Exception as e: messagebox.showerror("错误", f"公式测试失败:{str(e)}") # 添加按钮 ttk.Button(button_frame, text="测试公式", command=test_formula).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="保存公式", command=save_formula).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="取消", command=formula_window.destroy).pack(side=tk.RIGHT, padx=5) def get_resource_path(self, relative_path): """获取资源文件的绝对路径""" try: # PyInstaller创建临时文件夹,将路径存储在_MEIPASS中 base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) if __name__ == "__main__": root = tk.Tk() app = DataAnalysisApp(root) root.mainloop()
06-17
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"表头: {&#39;, &#39;.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}. {&#39;, &#39;.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}" 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") 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”) # 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") # 生成操作描述 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"以下文件不存在: {&#39;, &#39;.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" # 生成保存路径 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() 对这个代码的所有功能优化,要求 1.执行完功能操作输出的新的csv文件仍保留原数据默认表头 2.执行完功能操作输出的新的csv文件所自定义设定的表头 3.执行完功能操作输出的新的csv文件保留原数据默认表头和自定义设定的表头中间的数据 4.每个功能页增加勾选按钮,用户自主选择是否需要保留原数据表头 2.对于关键字要求缩小范围,不是只要有数据包含这个关键字就可以 并输出一份完整代码,不要省略代码
最新发布
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=(&#39;Arial&#39;, 9)) self.style.configure("TButton", font=(&#39;Arial&#39;, 9, &#39;bold&#39;)) self.style.configure("Accent.TButton", foreground="black", font=(&#39;Arial&#39;, 9, &#39;bold&#39;), 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=(&#39;Arial&#39;, 8), background="#ffcccc", borderwidth=1, relief="solid") self.style.map("Remove.TButton", background=[("active", "#ff9999"), ("!active", "#ffcccc")]) self.style.configure("Header.TCombobox", font=(&#39;Arial&#39;, 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=&#39;%(asctime)s - %(levelname)s - %(message)s&#39;, handlers=[ logging.FileHandler(&#39;csv_processor.log&#39;, encoding=&#39;utf-8&#39;), 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, &#39;r&#39;, encoding=&#39;utf-8-sig&#39;) 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[&#39;values&#39;] = 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"表头: {&#39;, &#39;.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}. {&#39;, &#39;.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[&#39;values&#39;] = 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[&#39;state&#39;] = state self.sort_order_combobox[&#39;state&#39;] = state self.toggle_letter_sort() self.log_message(f"排序功能 {&#39;启用&#39; if self.enable_sort.get() else &#39;禁用&#39;}") def toggle_dedupe(self): """切换去重功能的启用状态""" state = "normal" if self.enable_dedupe.get() else "disabled" self.dedupe_header_combobox[&#39;state&#39;] = state self.log_message(f"去重功能 {&#39;启用&#39; if self.enable_dedupe.get() else &#39;禁用&#39;}") 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[&#39;state&#39;] = state self.letter_range_end_entry[&#39;state&#39;] = state self.log_message(f"字母范围过滤 {&#39;启用&#39; if self.enable_custom_letter_sort.get() else &#39;禁用&#39;}") 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} 行包含 &#39;{keyword}&#39; 的数据") 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"排序完成,表头 &#39;{header}&#39;,顺序: {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"去重完成,根据表头 &#39;{header}&#39; 删除重复项," 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[&#39;USERPROFILE&#39;]), &#39;Desktop&#39;) if os.path.exists(desktop): return desktop except KeyError: pass # 如果上面的方法失败,尝试其他方法 desktop = os.path.join(os.path.expanduser(&#39;~&#39;), &#39;Desktop&#39;) 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, &#39;w&#39;, encoding=&#39;utf-8-sig&#39;, newline=&#39;&#39;) 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"以下文件不存在: {&#39;, &#39;.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, &#39;r&#39;, encoding=&#39;utf-8-sig&#39;) 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"合并依据列 &#39;{merge_column}&#39; 不在共同列中") 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() 对这个代码的所有功能优化,要求 1.执行完功能操作输出的新的csv文件仍保留原数据默认表头和所自定义设定的表头以及原数据默认表头和自定义设定的表头中间的数据, 2.对于关键字要求缩小范围,不是只要有数据包含这个关键字就可以 并输出一份完整代码
07-10
import os import sys import logging import time from pathlib import Path from concurrent.futures import ThreadPoolExecutor from tqdm import tqdm import tkinter as tk from tkinter import filedialog, messagebox, ttk from tkinterdnd2 import DND_FILES, TkinterDnD import json from pdf2docx import Converter import pytesseract from pdf2image import convert_from_path from docx import Document from docx.shared import Pt, Cm, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH import numexpr as ne import tempfile import traceback import io import threading import queue # 获取程序运行路径 def get_application_path(): """获取应用程序路径""" if getattr(sys, &#39;frozen&#39;, False): # 如果是打包后的exe return os.path.dirname(sys.executable) else: # 如果是直接运行的python脚本 return os.path.dirname(os.path.abspath(__file__)) # 设置NumExpr线程数 ne.set_num_threads(8) # 配置文件路径 CONFIG_FILE = os.path.join(get_application_path(), "config.json") # 日志文件路径 LOG_FILE = os.path.join(get_application_path(), "conversion.log") # 自定义StreamHandler来捕获所有输出 class StreamToLogger(io.StringIO): def __init__(self, logger, level): super().__init__() self.logger = logger self.level = level self.buf = &#39;&#39; def write(self, buf): self.buf = buf.strip(&#39;\r\n\t &#39;) if self.buf: self.logger.log(self.level, self.buf) def flush(self): pass def setup_logging(): """配置日志系统""" try: app_path = get_application_path() log_dir = os.path.join(app_path, &#39;logs&#39;) os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, &#39;conversion.log&#39;) # 创建logger logger = logging.getLogger() logger.setLevel(logging.DEBUG) # 创建文件处理器 file_handler = logging.FileHandler(log_file, mode=&#39;w&#39;, encoding=&#39;utf-8&#39;) file_handler.setLevel(logging.DEBUG) # 创建控制台处理器 console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) # 创建格式化器 formatter = logging.Formatter(&#39;%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s&#39;) file_handler.setFormatter(formatter) console_handler.setFormatter(formatter) # 添加处理器 logger.addHandler(file_handler) logger.addHandler(console_handler) logging.debug(f"日志系统初始化成功,日志文件路径:{log_file}") return logger except Exception as e: print(f"设置日志系统失败: {str(e)}") return None # 初始化日志记录器 logger = setup_logging() if not logger: print("警告:日志系统初始化失败,程序将继续运行但不会记录日志") # 创建一个基本的日志记录器 logger = logging.getLogger() logger.setLevel(logging.DEBUG) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(&#39;%(asctime)s - %(levelname)s - %(message)s&#39;)) logger.addHandler(handler) def load_config(): try: if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, &#39;r&#39;) as f: config = json.load(f) # 自动修正Poppler路径 poppler_path = config.get(&#39;POPPLER_PATH&#39;, &#39;&#39;) if poppler_path: # 检查是否存在bin子目录 bin_path = os.path.join(poppler_path, &#39;bin&#39;) if os.path.exists(bin_path): config[&#39;POPPLER_PATH&#39;] = bin_path return config return {} except: return {} def save_config(config): """保存配置文件""" with open(CONFIG_FILE, &#39;w&#39;, encoding=&#39;utf-8&#39;) as f: json.dump(config, f, ensure_ascii=False, indent=4) def setup_config(): """首次运行配置界面""" root = TkinterDnD.Tk() root.title("PDF转Word工具 - 首次配置") root.geometry("600x400") # 设置窗口样式 root.configure(bg=&#39;#f0f0f0&#39;) style = {&#39;bg&#39;: &#39;#f0f0f0&#39;, &#39;font&#39;: (&#39;微软雅黑&#39;, 10)} button_style = {&#39;bg&#39;: &#39;#4CAF50&#39;, &#39;fg&#39;: &#39;white&#39;, &#39;font&#39;: (&#39;微软雅黑&#39;, 10), &#39;padx&#39;: 10, &#39;pady&#39;: 5} # 创建说明标签 tk.Label(root, text="首次使用需要配置以下路径:", **style).pack(pady=10) # Poppler路径配置 poppler_frame = tk.Frame(root, bg=&#39;#f0f0f0&#39;) poppler_frame.pack(fill=&#39;x&#39;, padx=20, pady=5) tk.Label(poppler_frame, text="Poppler路径:", **style).pack(side=&#39;left&#39;) poppler_path = tk.StringVar() poppler_entry = tk.Entry(poppler_frame, textvariable=poppler_path, width=50) poppler_entry.pack(side=&#39;left&#39;, padx=5) def select_poppler(): path = filedialog.askdirectory(title="选择Poppler安装目录") if path: poppler_path.set(path) tk.Button(poppler_frame, text="浏览", command=select_poppler, **button_style).pack(side=&#39;left&#39;) # Tesseract路径配置 tesseract_frame = tk.Frame(root, bg=&#39;#f0f0f0&#39;) tesseract_frame.pack(fill=&#39;x&#39;, padx=20, pady=5) tk.Label(tesseract_frame, text="Tesseract路径:", **style).pack(side=&#39;left&#39;) tesseract_path = tk.StringVar() tesseract_entry = tk.Entry(tesseract_frame, textvariable=tesseract_path, width=50) tesseract_entry.pack(side=&#39;left&#39;, padx=5) def select_tesseract(): path = filedialog.askdirectory(title="选择Tesseract安装目录") if path: tesseract_path.set(path) tk.Button(tesseract_frame, text="浏览", command=select_tesseract, **button_style).pack(side=&#39;left&#39;) # 说明文本 help_text = """ 使用说明: 1. Poppler路径:选择poppler的安装目录(包含bin文件夹的目录) 2. Tesseract路径:选择Tesseract-OCR的安装目录 3. 配置完成后点击"保存配置"即可开始使用 4. Poppler和Tesseract需放在在系统C盘下(C:\Program Files\) """ tk.Label(root, text=help_text, justify=&#39;left&#39;, **style).pack(pady=10) def save_and_exit(): if not poppler_path.get() or not tesseract_path.get(): messagebox.showerror("错误", "请填写所有配置项!") return tesseract_dir = tesseract_path.get() tesseract_exe = os.path.join(tesseract_dir, &#39;tesseract.exe&#39;) # 新增:验证Tesseract路径有效性 if not os.path.isfile(tesseract_exe): messagebox.showerror("错误", "选择的Tesseract路径无效,未找到tesseract.exe!") return config = { &#39;POPPLER_PATH&#39;: poppler_path.get(), &#39;TESSERACT_CMD&#39;: tesseract_exe # 直接使用正确路径 } save_config(config) root.destroy() tk.Button(root, text="保存配置", command=save_and_exit, **button_style).pack(pady=20) root.mainloop() class PDFConverterGUI: def __init__(self): self.root = TkinterDnD.Tk() self.root.title("PDF转Word工具") self.root.geometry("800x600") # 设置样式 self.style = {&#39;bg&#39;: &#39;#f0f0f0&#39;, &#39;font&#39;: (&#39;微软雅黑&#39;, 10)} self.button_style = {&#39;bg&#39;: &#39;#4CAF50&#39;, &#39;fg&#39;: &#39;white&#39;, &#39;font&#39;: (&#39;微软雅黑&#39;, 10), &#39;padx&#39;: 10, &#39;pady&#39;: 5} # 绑定窗口关闭事件 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.setup_ui() def on_closing(self): """处理窗口关闭事件""" try: if messagebox.askokcancel("退出", "确定要退出程序吗?"): self.root.destroy() sys.exit(0) except Exception as e: logger.error(f"关闭窗口时出错: {str(e)}") self.root.destroy() sys.exit(1) def setup_ui(self): # 创建主框架 main_frame = tk.Frame(self.root, bg=&#39;#f0f0f0&#39;) main_frame.pack(fill=&#39;both&#39;, expand=True, padx=20, pady=20) # 路径选择区域 path_frame = tk.Frame(main_frame, bg=&#39;#f0f0f0&#39;) path_frame.pack(fill=&#39;x&#39;, pady=10) tk.Label(path_frame, text="选择PDF文件或目录:", **self.style).pack(side=&#39;left&#39;) self.path_var = tk.StringVar() path_entry = tk.Entry(path_frame, textvariable=self.path_var, width=50) path_entry.pack(side=&#39;left&#39;, padx=5) tk.Button(path_frame, text="选择文件", command=self.select_file, **self.button_style).pack(side=&#39;left&#39;, padx=5) tk.Button(path_frame, text="选择目录", command=self.select_directory, **self.button_style).pack(side=&#39;left&#39;) # 文件列表区域 list_frame = tk.Frame(main_frame, bg=&#39;#f0f0f0&#39;) list_frame.pack(fill=&#39;both&#39;, expand=True, pady=10) tk.Label(list_frame, text="待处理文件列表(支持拖放文件):", **self.style).pack(anchor=&#39;w&#39;) # 创建带滚动条的列表框 self.listbox_frame = tk.Frame(list_frame, bg=&#39;#f0f0f0&#39;) self.listbox_frame.pack(fill=&#39;both&#39;, expand=True) self.file_listbox = tk.Listbox(self.listbox_frame, width=80, height=15) self.file_listbox.pack(side=&#39;left&#39;, fill=&#39;both&#39;, expand=True) scrollbar = tk.Scrollbar(self.listbox_frame) scrollbar.pack(side=&#39;right&#39;, fill=&#39;y&#39;) self.file_listbox.config(yscrollcommand=scrollbar.set) scrollbar.config(command=self.file_listbox.yview) # 绑定拖放事件 self.file_listbox.drop_target_register(DND_FILES) self.file_listbox.dnd_bind(&#39;<<Drop>>&#39;, self.handle_drop) # 进度条 self.progress_var = tk.DoubleVar() self.progress = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100) self.progress.pack(fill=&#39;x&#39;, pady=10) # 状态标签 self.status_var = tk.StringVar(value="就绪") tk.Label(main_frame, textvariable=self.status_var, **self.style).pack(pady=5) # 按钮区域 button_frame = tk.Frame(main_frame, bg=&#39;#f0f0f0&#39;) button_frame.pack(pady=10) tk.Button(button_frame, text="开始转换", command=self.start_conversion, **self.button_style).pack(side=&#39;left&#39;, padx=5) tk.Button(button_frame, text="清空列表", command=self.clear_list, **self.button_style).pack(side=&#39;left&#39;, padx=5) tk.Button(button_frame, text="退出", command=self.root.quit, **self.button_style).pack(side=&#39;left&#39;, padx=5) # 添加日志显示区域 log_frame = tk.Frame(main_frame, bg=&#39;#f0f0f0&#39;) log_frame.pack(fill=&#39;both&#39;, expand=True, pady=10) tk.Label(log_frame, text="转换日志:", **self.style).pack(anchor=&#39;w&#39;) # 创建带滚动条的日志文本框 self.log_text = tk.Text(log_frame, wrap=tk.WORD, width=80, height=10) scrollbar = tk.Scrollbar(log_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.log_text.config(yscrollcommand=scrollbar.set) scrollbar.config(command=self.log_text.yview) # 配置日志队列和更新机制 self.log_queue = queue.Queue() self.setup_log_handler() self.root.after(100, self.update_log_display) def update_log_display(self): """定期更新日志显示""" while not self.log_queue.empty(): msg = self.log_queue.get() self.log_text.configure(state=&#39;normal&#39;) self.log_text.insert(tk.END, msg + &#39;\n&#39;) self.log_text.configure(state=&#39;disabled&#39;) # 自动滚动到底部 self.log_text.see(tk.END) self.root.after(100, self.update_log_display) def setup_log_handler(self): """配置GUI日志处理器""" class QueueHandler(logging.Handler): def __init__(self, queue): super().__init__() self.queue = queue def emit(self, record): msg = self.format(record) self.queue.put(msg) # 创建并添加自定义处理器 handler = QueueHandler(self.log_queue) handler.setFormatter(logging.Formatter(&#39;%(asctime)s - %(levelname)s - %(message)s&#39;)) logger.addHandler(handler) def handle_drop(self, event): """处理文件拖放事件""" files = event.data.split() for file in files: # 移除文件路径中的花括号(如果有) file = file.strip(&#39;{}&#39;) if file.lower().endswith(&#39;.pdf&#39;): if file not in self.file_listbox.get(0, tk.END): self.file_listbox.insert(tk.END, file) self.path_var.set(os.path.dirname(file)) elif os.path.isdir(file): # 如果是目录,则添加目录下的所有PDF文件 pdf_files = find_pdf_files(file) for pdf_file in pdf_files: if pdf_file not in self.file_listbox.get(0, tk.END): self.file_listbox.insert(tk.END, pdf_file) self.path_var.set(file) def select_file(self): files = filedialog.askopenfilenames( title="选择PDF文件", filetypes=[("PDF文件", "*.pdf")] ) if files: for file in files: if file not in self.file_listbox.get(0, tk.END): self.file_listbox.insert(tk.END, file) self.path_var.set(os.path.dirname(files[0])) def select_directory(self): directory = filedialog.askdirectory(title="选择包含PDF文件的目录") if directory: self.path_var.set(directory) self.clear_list() pdf_files = find_pdf_files(directory) for file in pdf_files: self.file_listbox.insert(tk.END, file) def clear_list(self): self.file_listbox.delete(0, tk.END) self.progress_var.set(0) self.status_var.set("就绪") def start_conversion(self): files = list(self.file_listbox.get(0, tk.END)) if not files: messagebox.showwarning("警告", "请先选择要转换的PDF文件!") return total_files = len(files) self.progress_var.set(0) def update_progress(current, total): try: progress = (current / total) * 100 self.progress_var.set(progress) self.status_var.set(f"正在处理: {current}/{total}") self.root.update() except Exception as e: logger.error(f"更新进度时出错: {str(e)}") for i, file in enumerate(files, 1): try: self.status_var.set(f"正在处理: {os.path.basename(file)}") self.root.update() if process_single_pdf(file): self.file_listbox.itemconfig(i - 1, {&#39;bg&#39;: &#39;#90EE90&#39;}) # 浅绿色表示成功 else: self.file_listbox.itemconfig(i - 1, {&#39;bg&#39;: &#39;#FFB6C1&#39;}) # 浅红色表示失败 update_progress(i, total_files) except Exception as e: logger.error(f"处理文件失败: {str(e)}", exc_info=True) self.file_listbox.itemconfig(i - 1, {&#39;bg&#39;: &#39;#FFB6C1&#39;}) self.status_var.set("转换完成!") messagebox.showinfo("完成", f"处理完成!共处理 {total_files} 个文件。") def validate_environment(): missing_deps = [] # 检查Poppler路径 if not os.path.isdir(POPPLER_PATH): missing_deps.append(f"Poppler路径不存在: {POPPLER_PATH}") else: # 检查是否存在关键可执行文件 required_files = [&#39;pdfinfo.exe&#39;, &#39;pdftoppm.exe&#39;] for file in required_files: if not os.path.isfile(os.path.join(POPPLER_PATH, file)): missing_deps.append(f"Poppler路径中缺失文件: {file}") # 检查Tesseract是否存在且是可执行文件 if not os.path.isfile(TESSERACT_CMD) or not os.access(TESSERACT_CMD, os.X_OK): missing_deps.append(f"Tesseract路径不正确或不可执行: {TESSERACT_CMD}") if missing_deps: for dep in missing_deps: logger.error(dep) messagebox.showerror("配置错误", "环境验证失败:\n" + "\n".join(missing_deps) + "\n请检查Poppler是否安装正确") return False return True def extract_text_from_image(image): """从图片中提取文字""" try: # 配置OCR参数 custom_config = r&#39;--oem 3 --psm 6 -l chi_sim&#39; text = pytesseract.image_to_string(image, config=custom_config) return text except Exception as e: logger.error(f"OCR识别出错: {str(e)}", exc_info=True) return "" def process_single_pdf(pdf_path): """处理单个PDF文件""" temp_file = None temp_path = None try: start_time = time.time() pdf_name = os.path.basename(pdf_path) word_path = os.path.splitext(pdf_path)[0] + &#39;.docx&#39; logger.info(f"▶ 开始处理: {pdf_name}") # 基础转换 cv = Converter(pdf_path) cv.convert(word_path) cv.close() logger.info(" 基础转换完成") # OCR处理 images = convert_from_path(pdf_path, poppler_path=POPPLER_PATH) total_pages = len(images) logger.info(f" 共检测到 {total_pages} 页需要OCR识别") # 创建临时文件来存储OCR识别的文本 temp_path = os.path.join(tempfile.gettempdir(), f&#39;ocr_text_{int(time.time())}.txt&#39;) temp_file = open(temp_path, &#39;w&#39;, encoding=&#39;utf-8&#39;) # 对每一页进行OCR识别 for idx, image in enumerate(images): logger.info(f" 正在处理第 {idx + 1}/{total_pages} 页") text = extract_text_from_image(image) if text.strip(): # 如果识别到文字 temp_file.write(f"第{idx + 1}页识别到的文字:\n{text}\n\n") # 确保所有内容都写入文件 temp_file.flush() temp_file.close() temp_file = None # 将OCR识别的文字添加到Word文档中 if os.path.exists(temp_path): with open(temp_path, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: ocr_text = f.read() if ocr_text.strip(): logger.info(" 正在将OCR识别的文字添加到Word文档...") # 打开已转换的Word文档 doc = Document(word_path) # 添加OCR识别的文字 doc.add_paragraph("\nOCR识别结果:") doc.add_paragraph(ocr_text) # 保存文档 doc.save(word_path) logger.info(f" 已添加OCR识别结果") cost_time = time.time() - start_time logger.info(f"✓ 处理完成,耗时 {cost_time:.2f} 秒\n") return True except Exception as e: logger.error(f"转换失败: {str(e)}", exc_info=True) return False finally: # 确保临时文件被关闭和删除 if temp_file and not temp_file.closed: try: temp_file.close() except: pass if temp_path and os.path.exists(temp_path): try: os.unlink(temp_path) except: pass def find_pdf_files(directory): """递归查找所有PDF文件""" pdf_files = [] try: for root, _, files in os.walk(directory): for file in files: if file.lower().endswith(&#39;.pdf&#39;): full_path = os.path.join(root, file) pdf_files.append(full_path) return pdf_files except Exception as e: logger.error(f"文件搜索失败: {str(e)}") return [] def main(): try: print("程序启动...") logger.info("程序启动") # 检查配置文件 config = load_config() if not config: print("首次运行,请进行配置...") logger.info("首次运行,启动配置界面") setup_config() config = load_config() if not config: print("配置失败,程序退出") logger.error("配置失败,程序退出") return # 设置全局变量 global POPPLER_PATH, TESSERACT_CMD, TESSDATA_PREFIX POPPLER_PATH = config.get(&#39;POPPLER_PATH&#39;) TESSERACT_CMD = config.get(&#39;TESSERACT_CMD&#39;) TESSERACT_DIR = os.path.dirname(TESSERACT_CMD) TESSDATA_PREFIX = os.path.join(TESSERACT_DIR, &#39;tessdata&#39;) os.environ[&#39;TESSDATA_PREFIX&#39;] = TESSDATA_PREFIX # 新增:设置pytesseract的tesseract路径 pytesseract.pytesseract.tesseract_cmd = TESSERACT_CMD # 添加这一行 print(f"Poppler路径: {POPPLER_PATH}") print(f"Tesseract路径: {TESSERACT_CMD}") # 验证环境 if not validate_environment(): print("环境验证失败") logger.error("环境验证失败") messagebox.showerror("错误", "环境配置不正确,请检查配置!") return # 启动GUI print("启动GUI界面...") logger.info("启动GUI界面") app = PDFConverterGUI() app.root.mainloop() except Exception as e: error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}" print(error_msg) logger.error(error_msg) messagebox.showerror("错误", f"程序运行出错:{str(e)}") finally: # 确保所有日志都被写入 for handler in logger.handlers: handler.flush() handler.close() if __name__ == "__main__": main()
05-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值