帮我分析以下代码逻辑
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import os
import glob
import sys
import traceback
import re
import subprocess
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
from openpyxl.drawing.image import Image as XLImage
import io
import tempfile
class CollapsibleFrame(ttk.Frame):
"""可折叠/展开的面板"""
def __init__(self, parent, title="", *args, **kwargs):
ttk.Frame.__init__(self, parent, *args, **kwargs)
self.title = title
self.is_expanded = tk.BooleanVar(value=True)
# 标题栏
self.header = ttk.Frame(self)
self.header.pack(fill="x", pady=(0, 5))
self.toggle_btn = ttk.Checkbutton(
self.header, text=title,
variable=self.is_expanded,
command=self.toggle,
style="Toggle.TButton"
)
self.toggle_btn.pack(side="left", padx=5)
# 内容区域
self.content = ttk.Frame(self)
self.content.pack(fill="both", expand=True)
# 初始状态
self.toggle()
# 创建自定义样式
style = ttk.Style()
style.configure("Toggle.TButton", font=("Arial", 10, "bold"))
def toggle(self):
"""切换面板状态"""
if self.is_expanded.get():
self.content.pack(fill="both", expand=True)
else:
self.content.pack_forget()
class TensileAnalyzer:
def __init__(self, root):
self.root = root
self.root.title("拉力数据分析系统")
self.root.geometry("1200x900")
self.root.state('zoomed') # 启动时最大化窗口
self.data = pd.DataFrame()
self.all_data_details = [] # 存储所有详细数据
self.figures = [] # 存储图表对象
self.risk_files = [] # 存储有风险的文件路径
self.setup_ui()
def setup_ui(self):
# 主框架
main_frame = ttk.Frame(self.root)
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# 左侧控制面板
control_frame = ttk.Frame(main_frame, width=350)
control_frame.pack(side="left", fill="y", padx=(0, 10))
# 文件选择区域
file_frame = ttk.LabelFrame(control_frame, text="数据源")
file_frame.pack(fill="x", pady=5)
# 文件夹选择
folder_frame = ttk.Frame(file_frame)
folder_frame.pack(fill="x", pady=5)
ttk.Label(folder_frame, text="数据文件夹:").pack(side="left", padx=5, pady=5)
self.folder_entry = ttk.Entry(folder_frame, width=30)
self.folder_entry.pack(side="left", padx=5, pady=5, fill="x", expand=True)
ttk.Button(folder_frame, text="浏览", command=self.select_folder, width=8).pack(side="left", padx=5, pady=5)
# 文件选择
file_sel_frame = ttk.Frame(file_frame)
file_sel_frame.pack(fill="x", pady=5)
ttk.Label(file_sel_frame, text="单个文件:").pack(side="left", padx=5, pady=5)
self.file_entry = ttk.Entry(file_sel_frame, width=30)
self.file_entry.pack(side="left", padx=5, pady=5, fill="x", expand=True)
ttk.Button(file_sel_frame, text="浏览", command=self.select_file, width=8).pack(side="left", padx=5, pady=5)
# 型号选择
model_frame = ttk.Frame(file_frame)
model_frame.pack(fill="x", pady=5)
ttk.Label(model_frame, text="产品型号:").pack(side="left", padx=(0, 5))
self.model_combobox = ttk.Combobox(model_frame, width=20)
self.model_combobox.pack(side="left", padx=(0, 5))
ttk.Button(model_frame, text="刷新型号", command=self.refresh_models, width=8).pack(side="left")
# 规格设置区域
spec_frame = ttk.LabelFrame(control_frame, text="规格设置")
spec_frame.pack(fill="x", pady=5)
ttk.Label(spec_frame, text="规格下限(LCL):").grid(row=0, column=0, padx=5, pady=5, sticky='w')
self.lcl_entry = ttk.Entry(spec_frame, width=10)
self.lcl_entry.grid(row=0, column=1, padx=5, pady=5, sticky='w')
self.lcl_entry.insert(0, "180") # 默认值
ttk.Label(spec_frame, text="规格上限(USL):").grid(row=0, column=2, padx=5, pady=5, sticky='w')
self.usl_entry = ttk.Entry(spec_frame, width=10)
self.usl_entry.grid(row=0, column=3, padx=5, pady=5, sticky='w')
self.usl_entry.insert(0, "250") # 默认值
# 风险阈值
ttk.Label(spec_frame, text="风险阈值(LCL+):").grid(row=1, column=0, padx=5, pady=5, sticky='w')
self.risk_threshold_entry = ttk.Entry(spec_frame, width=10)
self.risk_threshold_entry.grid(row=1, column=1, padx=5, pady=5, sticky='w')
self.risk_threshold_entry.insert(0, "30") # 默认值
# 日期范围
date_frame = ttk.LabelFrame(control_frame, text="日期范围")
date_frame.pack(fill="x", pady=5)
ttk.Label(date_frame, text="开始日期:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
self.start_date = ttk.Entry(date_frame, width=12)
self.start_date.grid(row=0, column=1, padx=5, pady=5, sticky='w')
# 设置默认日期为60天前
default_start_date = (datetime.now() - timedelta(days=60)).strftime("%Y-%m-%d")
self.start_date.insert(0, default_start_date)
ttk.Label(date_frame, text="结束日期:").grid(row=0, column=2, padx=5, pady=5, sticky='w')
self.end_date = ttk.Entry(date_frame, width=12)
self.end_date.grid(row=0, column=3, padx=5, pady=5, sticky='w')
self.end_date.insert(0, datetime.now().strftime("%Y-%m-%d"))
# 日期范围调整按钮
date_btn_frame = ttk.Frame(date_frame)
date_btn_frame.grid(row=1, column=0, columnspan=4, pady=5)
ttk.Button(date_btn_frame, text="最近7天", width=8,
command=lambda: self.set_date_range(7)).pack(side="left", padx=2)
ttk.Button(date_btn_frame, text="最近30天", width=8,
command=lambda: self.set_date_range(30)).pack(side="left", padx=2)
ttk.Button(date_btn_frame, text="最近60天", width=8,
command=lambda: self.set_date_range(60)).pack(side="left", padx=2)
ttk.Button(date_btn_frame, text="所有日期", width=8,
command=self.set_all_dates).pack(side="left", padx=2)
# 操作按钮区域
btn_frame = ttk.LabelFrame(control_frame, text="操作")
btn_frame.pack(fill="x", pady=5)
# 分析按钮
ttk.Button(btn_frame, text="分析数据", command=self.analyze_data, width=15).pack(pady=5)
# 调试模式
self.debug_var = tk.BooleanVar(value=True) # 默认启用调试模式
self.debug_check = ttk.Checkbutton(btn_frame, text="调试模式(显示详细信息)", variable=self.debug_var)
self.debug_check.pack(pady=2)
# 详细信息按钮
ttk.Button(btn_frame, text="查看详细信息", command=self.show_details, width=15).pack(pady=5)
# 保存按钮
ttk.Button(btn_frame, text="保存结果", command=self.save_results, width=15).pack(pady=5)
# 右侧结果显示区域
result_frame = ttk.Frame(main_frame)
result_frame.pack(side="right", fill="both", expand=True)
# 可折叠的结果面板
self.result_panel = CollapsibleFrame(result_frame, title="分析结果")
self.result_panel.pack(fill="both", expand=True, pady=5)
# 结果显示区域
self.result_text = scrolledtext.ScrolledText(
self.result_panel.content,
height=15,
width=80,
font=("Consolas", 10) # 使用等宽字体
)
self.result_text.pack(fill="both", expand=True, padx=5, pady=5)
# 风险文件区域
self.risk_frame = ttk.LabelFrame(self.result_panel.content, text="风险文件")
self.risk_frame.pack(fill="x", padx=5, pady=5)
# 图表面板
self.chart_panel = CollapsibleFrame(result_frame, title="分析图表")
self.chart_panel.pack(fill="both", expand=True, pady=5)
# 图表区域
self.canvas_frame = ttk.Frame(self.chart_panel.content)
self.canvas_frame.pack(fill="both", expand=True, padx=5, pady=5)
def set_date_range(self, days):
"""设置日期范围为最近N天"""
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
self.start_date.delete(0, tk.END)
self.start_date.insert(0, start_date.strftime("%Y-%m-%d"))
self.end_date.delete(0, tk.END)
self.end_date.insert(0, end_date.strftime("%Y-%m-%d"))
def set_all_dates(self):
"""设置日期范围为所有日期"""
self.start_date.delete(0, tk.END)
self.start_date.insert(0, "2000-01-01") # 足够早的日期
self.end_date.delete(0, tk.END)
self.end_date.insert(0, datetime.now().strftime("%Y-%m-%d"))
def select_folder(self):
folder_path = filedialog.askdirectory()
if folder_path:
self.folder_entry.delete(0, tk.END)
self.folder_entry.insert(0, folder_path)
self.file_entry.delete(0, tk.END) # 清除单个文件选择
self.refresh_models()
def select_file(self):
file_path = filedialog.askopenfilename(filetypes=[("Excel files", "*.xls;*.xlsx")])
if file_path:
self.file_entry.delete(0, tk.END)
self.file_entry.insert(0, file_path)
self.folder_entry.delete(0, tk.END) # 清除文件夹选择
self.refresh_models()
def refresh_models(self):
folder_path = self.folder_entry.get()
file_path = self.file_entry.get()
# 清空风险文件显示
for widget in self.risk_frame.winfo_children():
widget.destroy()
if folder_path:
path_type = "文件夹"
path = folder_path
if not os.path.exists(folder_path):
self.result_text.insert(tk.END, f"错误: 文件夹不存在: {folder_path}\n")
return
elif file_path:
path_type = "文件"
path = file_path
if not os.path.exists(file_path):
self.result_text.insert(tk.END, f"错误: 文件不存在: {file_path}\n")
return
else:
return
models = set()
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, f"扫描{path_type}: {path}\n")
file_paths = []
if folder_path:
file_paths = glob.glob(os.path.join(folder_path, "*.xls*"))
elif file_path:
file_paths = [file_path]
file_count = 0
for file_path in file_paths:
file_count += 1
file_name = os.path.basename(file_path)
self.result_text.insert(tk.END, f"发现文件: {file_name}\n")
self.result_text.see(tk.END)
self.root.update()
try:
# 尝试使用 openpyxl 读取所有 Excel 文件
try:
df_meta = pd.read_excel(file_path, sheet_name='Sheet2', header=None, nrows=15, engine='openpyxl')
except Exception as e:
try:
df_meta = pd.read_excel(file_path, sheet_name='Sheet2', header=None, nrows=15, engine='xlrd')
except Exception as e2:
self.result_text.insert(tk.END, f" 无法读取文件: {str(e2)}\n")
continue
# 查找规格型号行 - 更智能的搜索
model_value = None
for i in range(len(df_meta)):
# 检查是否包含"规格型号"或类似文本
if not pd.isna(df_meta.iloc[i, 0]) and "规格型号" in str(df_meta.iloc[i, 0]):
if len(df_meta.columns) > 2:
model_value = df_meta.iloc[i, 2]
break
# 检查是否包含"model"或类似文本
elif not pd.isna(df_meta.iloc[i, 0]) and any(word in str(df_meta.iloc[i, 0]).lower() for word in ["model", "type", "规格"]):
if len(df_meta.columns) > 2:
model_value = df_meta.iloc[i, 2]
break
if model_value is not None and pd.notna(model_value):
model_str = str(model_value).strip()
models.add(model_str)
self.result_text.insert(tk.END, f" 发现型号: {model_str}\n")
else:
self.result_text.insert(tk.END, " 未找到型号信息\n")
except Exception as e:
self.result_text.insert(tk.END, f" 处理文件出错: {str(e)}\n")
self.model_combobox['values'] = sorted(models)
if models:
self.model_combobox.current(0)
self.result_text.insert(tk.END, f"\n找到 {len(models)} 个型号\n")
else:
self.result_text.insert(tk.END, "\n未找到任何型号信息\n")
def validate_date(self, date_str):
try:
return datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
return None
def parse_test_date(self, date_str):
"""多种日期格式解析"""
date_str = str(date_str).strip()
# 尝试常见格式
formats = [
"%Y%m%d", # 20250424
"%Y-%m-%d", # 2025-04-24
"%Y/%m/%d", # 2025/04/24
"%m/%d/%Y", # 04/24/2025
"%d-%m-%Y", # 24-04-2025
]
for fmt in formats:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue
# 尝试提取数字日期
digits = re.sub(r'\D', '', date_str)
if len(digits) == 8:
try:
return datetime.strptime(digits, "%Y%m%d")
except ValueError:
pass
return None
def analyze_data(self):
try:
# 清除之前的图表和风险文件
for widget in self.canvas_frame.winfo_children():
widget.destroy()
for widget in self.risk_frame.winfo_children():
widget.destroy()
self.figures.clear()
self.risk_files = [] # 重置风险文件列表
folder_path = self.folder_entry.get()
file_path = self.file_entry.get()
target_model = self.model_combobox.get().strip()
start_date_str = self.start_date.get().strip()
end_date_str = self.end_date.get().strip()
debug_mode = self.debug_var.get()
# 获取规格限
try:
lcl = float(self.lcl_entry.get().strip())
except:
messagebox.showerror("错误", "请输入有效的规格下限(LCL)")
return
try:
usl = float(self.usl_entry.get().strip())
except:
messagebox.showerror("错误", "请输入有效的规格上限(USL)")
return
# 获取风险阈值
try:
risk_threshold = float(self.risk_threshold_entry.get().strip())
except:
risk_threshold = 30 # 默认值
# 确定数据源
if folder_path:
file_paths = glob.glob(os.path.join(folder_path, "*.xls*"))
source_type = "文件夹"
elif file_path:
file_paths = [file_path]
source_type = "文件"
else:
messagebox.showerror("错误", "请选择数据文件夹或文件")
return
if not os.path.exists(file_paths[0]):
messagebox.showerror("错误", f"路径不存在: {file_paths[0]}")
return
if not target_model:
messagebox.showerror("错误", "请选择产品型号")
return
start_date = self.validate_date(start_date_str)
end_date = self.validate_date(end_date_str)
if not start_date or not end_date:
messagebox.showerror("错误", "请填写有效的日期范围 (格式: YYYY-MM-DD)")
return
# 收集所有文件数据
self.all_data_details = [] # 重置详细数据
all_data = [] # 仅拉力值
failure_counts = {'A': 0, 'B': 0, 'C': 0, 'D': 0}
total_files = 0
processed_files = 0
matched_files = 0
included_files = 0
self.result_text.delete(1.0, tk.END)
self.result_text.insert(tk.END, f"开始分析...\n目标型号: {target_model}\n")
self.result_text.insert(tk.END, f"规格下限(LCL): {lcl} N, 规格上限(USL): {usl} N\n")
self.result_text.insert(tk.END, f"日期范围: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}\n")
self.result_text.insert(tk.END, f"数据源: {source_type} ({len(file_paths)}个文件)\n\n")
for file_path in file_paths:
total_files += 1
file_name = os.path.basename(file_path)
self.result_text.insert(tk.END, f"处理文件: {file_name}\n")
if debug_mode:
self.result_text.see(tk.END)
self.root.update()
try:
# 尝试使用 openpyxl 读取文件
try:
df_meta = pd.read_excel(file_path, sheet_name='Sheet2', header=None, nrows=20, engine='openpyxl')
except Exception as e:
try:
df_meta = pd.read_excel(file_path, sheet_name='Sheet2', header=None, nrows=20, engine='xlrd')
except Exception as e2:
self.result_text.insert(tk.END, f" 读取元数据失败: {str(e2)}\n")
continue
# 1. 获取型号
model_value = None
model_row_index = None
for i in range(len(df_meta)):
if not pd.isna(df_meta.iloc[i, 0]) and "规格型号" in str(df_meta.iloc[i, 0]):
if len(df_meta.columns) > 2:
model_value = df_meta.iloc[i, 2]
model_row_index = i
break
if model_value is None:
self.result_text.insert(tk.END, " 未找到型号信息\n")
continue
model_str = str(model_value).strip()
if debug_mode:
self.result_text.insert(tk.END, f" 文件型号: {model_str}\n")
if model_str != target_model:
if debug_mode:
self.result_text.insert(tk.END, f" 型号不匹配 (目标: {target_model})\n")
continue
matched_files += 1
# 2. 获取日期
test_date = None
date_row_index = None
for i in range(len(df_meta)):
if not pd.isna(df_meta.iloc[i, 0]) and "试验日期" in str(df_meta.iloc[i, 0]):
if len(df_meta.columns) > 2:
date_value = df_meta.iloc[i, 2]
if date_value is not None:
test_date = self.parse_test_date(date_value)
date_row_index = i
break
if test_date is None:
# 尝试从文件名中提取日期
try:
# 匹配文件名中的日期模式 (如 ACP-3S-2025.4.24.xls)
match = re.search(r'(\d{4})[\.\-]?(\d{1,2})[\.\-]?(\d{1,2})', file_name)
if match:
year = int(match.group(1))
month = int(match.group(2))
day = int(match.group(3))
test_date = datetime(year, month, day)
self.result_text.insert(tk.END, f" 从文件名解析日期: {test_date.strftime('%Y-%m-%d')}\n")
except:
self.result_text.insert(tk.END, " 未找到有效日期\n")
continue
if test_date is None:
self.result_text.insert(tk.END, " 未找到有效日期\n")
continue
if debug_mode:
self.result_text.insert(tk.END, f" 文件日期: {test_date.strftime('%Y-%m-%d')}\n")
# 检查日期范围
if test_date < start_date or test_date > end_date:
if debug_mode:
self.result_text.insert(tk.END, f" 日期不在范围内 (要求: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')})\n")
continue
included_files += 1
# 3. 定位数据起始行 - 改进版本
data_start_row = None
# 查找包含"序号"或"No"的行
for i in range(len(df_meta)):
if not pd.isna(df_meta.iloc[i, 0]) and any(keyword in str(df_meta.iloc[i, 0]) for keyword in ["序号", "No", "样本"]):
data_start_row = i + 1
break
# 如果没找到,尝试查找数值起始行
if data_start_row is None:
for i in range(len(df_meta)):
if pd.api.types.is_number(df_meta.iloc[i, 0]) and not pd.isna(df_meta.iloc[i, 0]):
data_start_row = i
break
if data_start_row is None:
# 尝试基于型号行和日期行定位
if model_row_index is not None and date_row_index is not None:
data_start_row = max(model_row_index, date_row_index) + 5
else:
data_start_row = 10 # 默认值
if debug_mode:
self.result_text.insert(tk.END, f" 数据起始行: {data_start_row}\n")
# 4. 读取数据
try:
# 尝试读取数据部分
df_data = pd.read_excel(
file_path,
sheet_name='Sheet2',
skiprows=data_start_row,
header=None,
engine='openpyxl'
)
except:
try:
df_data = pd.read_excel(
file_path,
sheet_name='Sheet2',
skiprows=data_start_row,
header=None,
engine='xlrd'
)
except Exception as e:
self.result_text.insert(tk.END, f" 读取数据失败: {str(e)}\n")
continue
if debug_mode:
self.result_text.insert(tk.END, f" 找到 {len(df_data)} 行数据\n")
if len(df_data) > 0:
self.result_text.insert(tk.END, f" 前3行数据预览:\n")
for i in range(min(3, len(df_data))):
self.result_text.insert(tk.END, f" 行 {i+1}: {df_data.iloc[i].values}\n")
# 5. 处理数据 - 改进版本
sample_count = 0
file_has_risk = False # 标记文件是否有风险数据
for index, row in df_data.iterrows():
# 检查是否为有效数据行
if len(row) < 4: # 确保有足够的列
continue
# 检查序号列是否为数字
try:
if pd.isna(row[0]) or not isinstance(row[0], (int, float)):
continue
except:
continue
# 获取拉力值 (B列)
force = row[1]
if not pd.api.types.is_number(force) or pd.isna(force):
continue
# 获取失效模式 (D列)
failure_mode = row[3]
if pd.isna(failure_mode):
failure_mode = None
else:
failure_mode = str(failure_mode).strip()
if len(failure_mode) > 0:
failure_mode = failure_mode[0] # 只取第一个字母
else:
failure_mode = None
# 检查是否低于风险阈值
if force < lcl + risk_threshold:
file_has_risk = True
# 添加到数据集
all_data.append(force)
sample_count += 1
# 添加到详细数据
self.all_data_details.append({
'文件名称': file_name,
'文件路径': file_path, # 保存完整路径用于风险文件打开
'测试日期': test_date.strftime('%Y-%m-%d'),
'序号': row[0],
'拉力值(N)': force,
'失效模式': failure_mode
})
# 统计失效模式
if failure_mode and failure_mode in failure_counts:
failure_counts[failure_mode] += 1
# 如果有风险数据,添加到风险文件列表
if file_has_risk:
self.risk_files.append(file_path)
processed_files += 1
self.result_text.insert(tk.END, f" 成功读取 {sample_count} 个样本\n")
except Exception as e:
error_msg = f" 处理文件出错: {str(e)}"
self.result_text.insert(tk.END, error_msg + "\n")
if debug_mode:
traceback.print_exc()
continue
# 结果汇总
self.result_text.insert(tk.END, "\n===== 分析结果汇总 =====\n")
self.result_text.insert(tk.END, f"扫描文件总数: {total_files}\n")
self.result_text.insert(tk.END, f"型号匹配文件数: {matched_files}\n")
self.result_text.insert(tk.END, f"日期范围内文件数: {included_files}\n")
self.result_text.insert(tk.END, f"成功处理文件数: {processed_files}\n")
self.result_text.insert(tk.END, f"总样本数量: {len(all_data)}\n")
if not all_data:
# 给出调整日期范围的建议
self.result_text.insert(tk.END, "\n没有找到符合条件的数据,建议:\n")
self.result_text.insert(tk.END, f"1. 调整日期范围(当前: {start_date_str} 至 {end_date_str})\n")
self.result_text.insert(tk.END, "2. 使用'所有日期'按钮扩大范围\n")
self.result_text.insert(tk.END, "3. 检查文件夹是否包含所需文件\n")
return
# 计算统计指标
data_series = pd.Series(all_data)
mean_val = data_series.mean()
median_val = data_series.median()
std_val = data_series.std()
min_val = data_series.min()
max_val = data_series.max()
# 计算过程能力
cp = (usl - lcl) / (6 * std_val) if std_val > 0 else 0
cpk = min((mean_val - lcl) / (3 * std_val), (usl - mean_val) / (3 * std_val)) if std_val > 0 else 0
# 计算失效模式百分比
total_failures = sum(failure_counts.values())
failure_percent = {k: (v / total_failures * 100) if total_failures > 0 else 0
for k, v in failure_counts.items()}
# 显示详细结果
result = f"""
=== 详细分析结果 ===
产品型号: {target_model}
规格范围: {lcl} N ~ {usl} N
日期范围: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}
样本数量: {len(all_data)}
风险阈值: LCL + {risk_threshold} = {lcl + risk_threshold:.1f} N
统计指标:
最小值: {min_val:.2f} N
最大值: {max_val:.2f} N
平均值: {mean_val:.2f} N
中值: {median_val:.2f} N
标准差: {std_val:.2f} N
过程能力指数(CP): {cp:.3f}
过程能力指数(CPK): {cpk:.3f}
失效模式统计:
A模式: {failure_counts['A']} 颗 ({failure_percent['A']:.1f}%)
B模式: {failure_counts['B']} 颗 ({failure_percent['B']:.1f}%)
C模式: {failure_counts['C']} 颗 ({failure_percent['C']:.1f}%)
D模式: {failure_counts['D']} 颗 ({failure_percent['D']:.1f}%)
"""
self.result_text.insert(tk.END, result)
# 显示风险文件
self.show_risk_files(lcl, risk_threshold)
self.result_text.see(tk.END) # 滚动到底部
# 绘制图表
self.plot_data(data_series, target_model, lcl, usl, mean_val, std_val, risk_threshold)
except Exception as e:
error_msg = f"分析出错: {str(e)}"
messagebox.showerror("错误", error_msg)
self.result_text.insert(tk.END, f"\n{error_msg}")
self.result_text.see(tk.END)
traceback.print_exc()
def show_risk_files(self, lcl, risk_threshold):
"""显示包含风险数据的文件"""
# 清空风险文件区域
for widget in self.risk_frame.winfo_children():
widget.destroy()
if not self.risk_files:
ttk.Label(self.risk_frame, text="未检测到风险文件").pack(padx=5, pady=5)
return
# 显示风险文件标题
risk_title = ttk.Label(
self.risk_frame,
text=f"检测到 {len(self.risk_files)} 个文件包含拉力值 < LCL+{risk_threshold} = {lcl+risk_threshold:.1f}N 的数据:",
font=("Arial", 10, "bold")
)
risk_title.pack(anchor="w", padx=5, pady=5)
# 创建滚动区域
risk_canvas = tk.Canvas(self.risk_frame, height=100)
scrollbar = ttk.Scrollbar(self.risk_frame, orient="vertical", command=risk_canvas.yview)
scrollable_frame = ttk.Frame(risk_canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: risk_canvas.configure(scrollregion=risk_canvas.bbox("all"))
)
risk_canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
risk_canvas.configure(yscrollcommand=scrollbar.set)
risk_canvas.pack(side="left", fill="both", expand=True, padx=5, pady=5)
scrollbar.pack(side="right", fill="y")
# 添加文件列表
for i, file_path in enumerate(self.risk_files):
file_name = os.path.basename(file_path)
file_frame = ttk.Frame(scrollable_frame)
file_frame.pack(fill="x", padx=5, pady=2)
ttk.Label(file_frame, text=f"{i+1}. {file_name}").pack(side="left")
# 添加打开按钮
open_btn = ttk.Button(
file_frame,
text="打开文件",
width=10,
command=lambda path=file_path: self.open_file(path)
)
open_btn.pack(side="right", padx=5)
def open_file(self, file_path):
"""用系统默认程序打开文件"""
if not os.path.exists(file_path):
messagebox.showerror("错误", f"文件不存在: {file_path}")
return
try:
if sys.platform.startswith('win'):
os.startfile(file_path)
elif sys.platform.startswith('darwin'):
subprocess.call(('open', file_path))
else:
subprocess.call(('xdg-open', file_path))
except Exception as e:
messagebox.showerror("错误", f"无法打开文件: {str(e)}")
def plot_data(self, data, model, lcl, usl, mean_val, std_val, risk_threshold):
# 清除旧图表
for widget in self.canvas_frame.winfo_children():
widget.destroy()
# 创建图表框架
fig = plt.Figure(figsize=(12, 8))
fig.suptitle(f"{model} 拉力数据分析", fontsize=16)
# 趋势图
ax1 = fig.add_subplot(211)
ax1.plot(data.values, marker='o', linestyle='-', color='b')
ax1.set_title('拉力值趋势图', fontsize=12)
ax1.set_xlabel('样本序号', fontsize=10)
ax1.set_ylabel('拉力值 (N)', fontsize=10)
ax1.grid(True)
# 添加统计线和规格线
ax1.axhline(y=mean_val, color='r', linestyle='-', label=f'平均值: {mean_val:.2f}N')
ax1.axhline(y=mean_val + std_val, color='g', linestyle='--', label=f'+1标准差')
ax1.axhline(y=mean_val - std_val, color='g', linestyle='--', label=f'-1标准差')
ax1.axhline(y=lcl, color='purple', linestyle='-.', label=f'规格下限(LCL): {lcl}N')
ax1.axhline(y=usl, color='orange', linestyle='-.', label=f'规格上限(USL): {usl}N')
ax1.axhline(y=lcl + risk_threshold, color='brown', linestyle=':', label=f'风险阈值: {lcl+risk_threshold:.1f}N')
ax1.legend()
# 直方图
ax2 = fig.add_subplot(212)
ax2.hist(data, bins=10, color='skyblue', edgecolor='black')
ax2.set_title('拉力值分布', fontsize=12)
ax2.set_xlabel('拉力值 (N)', fontsize=10)
ax2.set_ylabel('频次', fontsize=10)
ax2.grid(True)
# 添加统计值和规格线
ax2.axvline(x=lcl, color='purple', linestyle='-.', label=f'LCL: {lcl}N')
ax2.axvline(x=usl, color='orange', linestyle='-.', label=f'USL: {usl}N')
ax2.axvline(x=lcl + risk_threshold, color='brown', linestyle=':', label=f'风险阈值: {lcl+risk_threshold:.1f}N')
ax2.axvline(x=mean_val, color='r', linestyle='-', label=f'平均值: {mean_val:.2f}N')
ax2.legend()
# 添加统计值
stats_text = f"样本数: {len(data)}\n最小值: {data.min():.2f} N\n最大值: {data.max():.2f} N\n平均值: {data.mean():.2f} N\n标准差: {data.std():.2f} N"
ax2.text(0.95, 0.95, stats_text, transform=ax2.transAxes,
verticalalignment='top', horizontalalignment='right',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5),
fontsize=9)
plt.tight_layout(rect=[0, 0, 1, 0.95])
# 嵌入到Tkinter
canvas = FigureCanvasTkAgg(fig, master=self.canvas_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)
# 保存图表对象
self.figures.append(fig)
def show_details(self):
"""显示详细数据"""
if not self.all_data_details:
messagebox.showinfo("提示", "没有可用的详细数据,请先执行分析")
return
# 创建临时Excel文件
temp_file = tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False)
temp_path = temp_file.name
temp_file.close()
try:
# 创建Excel工作簿
wb = Workbook()
ws = wb.active
ws.title = "详细数据"
# 添加标题行
headers = ["文件名称", "测试日期", "序号", "拉力值(N)", "失效模式"]
ws.append(headers)
# 添加数据
for item in self.all_data_details:
ws.append([
item['文件名称'],
item['测试日期'],
item['序号'],
item['拉力值(N)'],
item['失效模式']
])
# 添加统计结果
ws.append([])
ws.append(["统计分析结果"])
ws.append(["产品型号", self.model_combobox.get().strip()])
ws.append(["规格下限(LCL)", float(self.lcl_entry.get())])
ws.append(["规格上限(USL)", float(self.usl_entry.get())])
# 获取文本结果
result_text = self.result_text.get("1.0", tk.END)
result_lines = result_text.split('\n')
for line in result_lines:
if "===" in line or line.strip() == "":
continue
ws.append([line])
# 保存Excel文件
wb.save(temp_path)
# 打开Excel文件
if sys.platform.startswith('win'):
os.startfile(temp_path)
elif sys.platform.startswith('darwin'):
subprocess.call(('open', temp_path))
else:
subprocess.call(('xdg-open', temp_path))
except Exception as e:
messagebox.showerror("错误", f"创建详细数据文件失败: {str(e)}")
def save_results(self):
"""保存分析结果"""
if not self.all_data_details or not self.figures:
messagebox.showinfo("提示", "没有可保存的结果,请先执行分析")
return
# 选择保存位置
save_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")],
title="保存分析结果"
)
if not save_path:
return
try:
# 创建Excel工作簿
wb = Workbook()
# 添加详细数据工作表
ws_data = wb.active
ws_data.title = "详细数据"
# 添加标题行
headers = ["文件名称", "测试日期", "序号", "拉力值(N)", "失效模式"]
ws_data.append(headers)
# 添加数据
for item in self.all_data_details:
ws_data.append([
item['文件名称'],
item['测试日期'],
item['序号'],
item['拉力值(N)'],
item['失效模式']
])
# 添加统计结果工作表
ws_stats = wb.create_sheet("统计分析")
# 添加规格和日期信息
ws_stats.append(["产品型号", self.model_combobox.get().strip()])
ws_stats.append(["规格下限(LCL)", float(self.lcl_entry.get())])
ws_stats.append(["规格上限(USL)", float(self.usl_entry.get())])
ws_stats.append(["开始日期", self.start_date.get()])
ws_stats.append(["结束日期", self.end_date.get()])
ws_stats.append([])
# 添加统计结果
result_text = self.result_text.get("1.0", tk.END)
result_lines = result_text.split('\n')
for line in result_lines:
if line.strip() == "":
continue
ws_stats.append([line])
# 添加图表工作表
ws_charts = wb.create_sheet("分析图表")
# 保存图表到临时文件并插入Excel
for i, fig in enumerate(self.figures):
# 保存图表为图片
img_data = io.BytesIO()
fig.savefig(img_data, format='png', dpi=100)
img_data.seek(0)
# 创建临时图片文件
img_temp = tempfile.NamedTemporaryFile(suffix='.png', delete=False)
img_temp.write(img_data.read())
img_temp.close()
# 插入图片到Excel
img = XLImage(img_temp.name)
img.anchor = f'A{1 + i * 25}' # 每张图间隔25行
ws_charts.add_image(img)
# 删除临时文件
os.unlink(img_temp.name)
# 保存Excel文件
wb.save(save_path)
messagebox.showinfo("成功", f"分析结果已保存到: {save_path}")
except Exception as e:
messagebox.showerror("错误", f"保存结果失败: {str(e)}")
def check_dependencies():
missing = []
try:
import pandas
except ImportError:
missing.append("pandas")
try:
import matplotlib
except ImportError:
missing.append("matplotlib")
try:
import openpyxl
except ImportError:
missing.append("openpyxl")
try:
import xlrd
except ImportError:
missing.append("xlrd")
if missing:
messagebox.showerror(
"缺少依赖库",
f"缺少必要的Python库: {', '.join(missing)}\n\n"
"请运行以下命令安装:\n"
"pip install pandas matplotlib openpyxl xlrd"
)
return False
return True
if __name__ == "__main__":
root = tk.Tk()
# 检查依赖库
if not check_dependencies():
root.destroy()
sys.exit(1)
app = TensileAnalyzer(root)
root.mainloop()