import os
import re
import sys
import tkinter as tk
from tkinter import ttk, Frame, Scrollbar, Canvas
from tkinter import scrolledtext, messagebox
import pandas as pd
import numpy as np
from glob import glob
import seaborn as sns
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.ticker as ticker
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from matplotlib.figure import Figure
plt.switch_backend("TkAgg")
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams["axes.unicode_minus"] = False
def find_batch_folders(path, batch_number):
target_dir = "4WCSVLog"
batch_folders = []
for root, dirs, files in os.walk(path):
path_components = os.path.normpath(root).split(os.sep)
if target_dir in path_components and batch_number in dirs:
full_path = os.path.join(root, batch_number)
batch_folders.append(full_path)
return batch_folders
def get_csv_files_path1(folder):
all_files = glob(os.path.join(folder, "*-SVP1B*.csv"))
return [f for f in all_files if "HEAD" not in f]
def get_csv_files_path2(folder):
return glob(os.path.join(folder, "*-SVP1B*.csv"))
def parse_svp1b(filename):
match = re.search(r"svp1b([0-9A-Z])([0-9])([0-9])", filename, re.IGNORECASE)
if match:
piece, line, file_num = match.groups()
piece_num = 10 + ord(piece.upper()) - ord("A") if piece.isalpha() else int(piece)
return piece_num, int(line), int(file_num)
return None
def read_csv_file(file, start_row=0, start_col=0):
df = pd.read_csv(file, header=None, skiprows=start_row)
return df.iloc[:, start_col:] if start_col > 0 else df
def merge_files(files, path_type):
piece_dict = {}
for file in files:
svp_info = parse_svp1b(os.path.basename(file).lower())
if not svp_info:
continue
piece, line, file_num = svp_info
key = (piece, line)
df = read_csv_file(file, start_row=(2 if path_type == 1 else 4),
start_col=(0 if path_type == 1 else 18))
if df.empty:
continue
if key not in piece_dict:
piece_dict[key] = []
piece_dict[key].append((file_num, df))
sorted_keys = sorted(piece_dict.keys(), key=lambda x: (x[0], x[1]))
global_col_counter = 1
final_dfs = []
for key in sorted_keys:
file_dfs = sorted(piece_dict[key], key=lambda x: x[0])
base_df = None
for file_num, df in file_dfs:
if base_df is None:
new_columns = [f"net{global_col_counter + i}" for i in range(len(df.columns))]
global_col_counter += len(df.columns)
df.columns = new_columns
base_df = df
else:
new_columns = [f"net{global_col_counter + i}" for i in range(len(df.columns))]
global_col_counter += len(df.columns)
df.columns = new_columns
base_df = pd.concat([base_df, df], axis=1)
if base_df is not None:
final_dfs.append(base_df)
return pd.concat(final_dfs, axis=0, ignore_index=True) if final_dfs else pd.DataFrame()
def collect_batch_data(batch_list):
batch_data_dict = {}
path1 = r"\\10.127.1.248\c1tst\7750"
path2 = r"\\10.127.1.248\c1tst\7755"
for idx, batch_number in enumerate(batch_list, 1):
print(f"=== 处理第 {idx}/{len(batch_list)} 个批次:{batch_number} ===")
batch_data = pd.DataFrame()
folders1 = find_batch_folders(path1, batch_number)
if folders1:
for folder1 in folders1:
files = get_csv_files_path1(folder1)
if files:
current_data = merge_files(files, path_type=1)
batch_data = pd.concat([batch_data, current_data], axis=0, ignore_index=True)
print(f"从路径1找到 {len(files)} 个数据文件")
else:
folders2 = find_batch_folders(path2, batch_number)
if folders2:
for folder2 in folders2:
files = get_csv_files_path2(folder2)
if files:
current_data = merge_files(files, path_type=2)
batch_data = pd.concat([batch_data, current_data], axis=0, ignore_index=True)
print(f"从路径2找到 {len(files)} 个数据文件")
else:
print(f"批次 {batch_number}:未找到对应文件夹,跳过\n")
continue
if not batch_data.empty:
batch_data_dict[batch_number] = batch_data
print(f"批次 {batch_number} 数据合并完成,共 {len(batch_data)} 行\n")
else:
print(f"批次 {batch_number}:无有效数据,跳过\n")
return batch_data_dict
class RedirectText:
def __init__(self, text_widget):
self.text_widget = text_widget
def write(self, string):
self.text_widget.insert(tk.END, string)
self.text_widget.see(tk.END)
def flush(self):
pass
class NetPlotViewer:
"""高级NET图表查看器,支持分页和无限滚动"""
def __init__(self, root, batch_data_dict, batch_list, net_list):
self.root = root
self.batch_data_dict = batch_data_dict
self.batch_list = batch_list
self.net_list = net_list
# 预先计算并缓存所有NET的数据
self.net_data_dict = self._precompute_net_data()
# 分页配置
self.per_page = 10
self.current_page = 0
self.total_pages = (len(net_list) + self.per_page - 1) // self.per_page
# 无限滚动配置
self.loaded_count = 0
self.load_step = 10
self.loading = False
self.view_mode = "pagination" # 或 "infinite_scroll"
# 创建主窗口
self.create_main_window()
def _precompute_net_data(self):
"""预先计算并缓存所有NET的数据"""
net_data_dict = {}
for net in self.net_list:
net_data = pd.DataFrame()
for batch in self.batch_list:
if batch in self.batch_data_dict and net in self.batch_data_dict[batch].columns:
net_data[f"批次 {batch}"] = self.batch_data_dict[batch][net]
net_data_dict[net] = net_data
return net_data_dict
def create_main_window(self):
"""创建主窗口和控件"""
self.plot_window = tk.Toplevel(self.root)
self.plot_window.title("批次数据分布箱线图")
self.plot_window.geometry("1200x800")
# 创建顶部控制面板
self.create_control_panel()
# 创建内容区域
self.create_content_area()
# 初始显示
self.update_display()
def create_control_panel(self):
"""创建顶部控制面板"""
control_frame = tk.Frame(self.plot_window, bd=1, relief=tk.RIDGE, padx=10, pady=10)
control_frame.pack(fill=tk.X, padx=10, pady=(10, 5))
# 视图模式选择
tk.Label(control_frame, text="视图模式:", font=("SimHei", 10)).pack(side=tk.LEFT, padx=5)
self.view_mode_var = tk.StringVar(value="分页视图" if self.view_mode == "pagination" else "无限滚动")
view_mode_menu = ttk.Combobox(
control_frame,
textvariable=self.view_mode_var,
values=["分页视图", "无限滚动"],
width=12,
state="readonly"
)
view_mode_menu.pack(side=tk.LEFT, padx=5)
view_mode_menu.bind("<<ComboboxSelected>>", self.change_view_mode)
# 分页控制
self.pagination_frame = tk.Frame(control_frame)
self.pagination_frame.pack(side=tk.LEFT, padx=20)
# 分页控件
self.prev_btn = tk.Button(
self.pagination_frame,
text="上一页",
command=lambda: self.change_page(-1),
state=tk.DISABLED
)
self.prev_btn.pack(side=tk.LEFT, padx=5)
self.page_label = tk.Label(self.pagination_frame, text="1/1", font=("SimHei", 10))
self.page_label.pack(side=tk.LEFT, padx=5)
self.next_btn = tk.Button(
self.pagination_frame,
text="下一页",
command=lambda: self.change_page(1),
state=tk.NORMAL if self.total_pages > 1 else tk.DISABLED
)
self.next_btn.pack(side=tk.LEFT, padx=5)
# 跳转控件
tk.Label(self.pagination_frame, text="跳转到:", font=("SimHei", 9)).pack(side=tk.LEFT, padx=(20, 0))
self.page_entry = tk.Entry(self.pagination_frame, width=4, font=("SimHei", 9))
self.page_entry.pack(side=tk.LEFT, padx=2)
self.page_entry.insert(0, "1")
self.goto_btn = tk.Button(
self.pagination_frame,
text="跳转",
command=self.goto_page,
font=("SimHei", 9)
)
self.goto_btn.pack(side=tk.LEFT, padx=2)
# 每页数量选择
tk.Label(self.pagination_frame, text="每页数量:", font=("SimHei", 9)).pack(side=tk.LEFT, padx=(20, 0))
self.per_page_var = tk.StringVar(value=str(self.per_page))
self.per_page_dropdown = ttk.Combobox(
self.pagination_frame,
textvariable=self.per_page_var,
values=["5", "10", "20", "50"],
width=4,
state="readonly"
)
self.per_page_dropdown.pack(side=tk.LEFT, padx=2)
self.per_page_dropdown.bind("<<ComboboxSelected>>", self.change_per_page)
# 关闭按钮
close_btn = tk.Button(
control_frame,
text="关闭窗口",
command=self.plot_window.destroy,
bg="#f44336",
fg="white",
font=("SimHei", 10),
)
close_btn.pack(side=tk.RIGHT, padx=10)
# 状态标签
self.status_label = tk.Label(control_frame, text="", font=("SimHei", 9), fg="#666")
self.status_label.pack(side=tk.RIGHT, padx=10)
def create_content_area(self):
"""创建内容显示区域"""
# 创建滚动框架
self.scroll_frame = tk.Frame(self.plot_window)
self.scroll_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
# 添加垂直滚动条
self.vscrollbar = tk.Scrollbar(self.scroll_frame, orient=tk.VERTICAL)
self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 创建画布用于滚动
self.canvas = tk.Canvas(self.scroll_frame, yscrollcommand=self.vscrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.vscrollbar.config(command=self.canvas.yview)
# 创建内容框架
self.content_frame = tk.Frame(self.canvas)
self.canvas_frame = self.canvas.create_window((0, 0), window=self.content_frame, anchor="nw")
# 绑定配置事件
self.content_frame.bind("<Configure>", self.on_frame_configure)
# 绑定鼠标滚轮事件
self.canvas.bind_all("<MouseWheel>", self.on_mousewheel)
def on_frame_configure(self, event=None):
"""更新滚动区域"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
# 检查是否需要加载更多(无限滚动模式)
if self.view_mode == "infinite_scroll":
self.check_load_more()
def on_mousewheel(self, event):
"""处理鼠标滚轮事件"""
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
# 检查是否需要加载更多(无限滚动模式)
if self.view_mode == "infinite_scroll":
self.check_load_more()
def change_view_mode(self, event=None):
"""切换视图模式"""
new_mode = "pagination" if self.view_mode_var.get() == "分页视图" else "infinite_scroll"
if new_mode != self.view_mode:
self.view_mode = new_mode
self.update_display()
# 更新分页控件的可见性
if self.view_mode == "pagination":
self.pagination_frame.pack(side=tk.LEFT, padx=20)
else:
self.pagination_frame.pack_forget()
def change_per_page(self, event=None):
"""更改每页显示数量"""
try:
new_per_page = int(self.per_page_var.get())
if new_per_page != self.per_page:
self.per_page = new_per_page
self.total_pages = (len(self.net_list) + self.per_page - 1) // self.per_page
self.current_page = 0
self.update_display()
except ValueError:
pass
def change_page(self, delta):
"""翻页"""
new_page = max(0, min(self.total_pages - 1, self.current_page + delta))
if new_page != self.current_page:
self.current_page = new_page
self.update_display()
def goto_page(self):
"""跳转到指定页码"""
try:
page_num = int(self.page_entry.get()) - 1
if 0 <= page_num < self.total_pages:
self.current_page = page_num
self.update_display()
else:
messagebox.showerror("错误", f"页码必须在 1 到 {self.total_pages} 之间")
except ValueError:
messagebox.showerror("错误", "请输入有效的页码数字")
def check_load_more(self):
"""检查是否需要加载更多(无限滚动模式)"""
if self.view_mode != "infinite_scroll" or self.loading:
return
# 获取滚动位置
scroll_position = self.canvas.yview()
# 如果滚动到底部附近(90%位置),加载更多
if scroll_position[1] > 0.9 and self.loaded_count < len(self.net_list):
self.load_more()
def load_more(self):
"""加载更多图表(无限滚动模式)"""
if self.loading or self.loaded_count >= len(self.net_list):
return
self.loading = True
# 显示加载状态
self.status_label.config(text=f"正在加载数据... ({self.loaded_count}/{len(self.net_list)})")
self.plot_window.update()
start_idx = self.loaded_count
end_idx = min(self.loaded_count + self.load_step, len(self.net_list))
current_nets = self.net_list[start_idx:end_idx]
# 创建加载动画
loading_label = tk.Label(
self.content_frame,
text="加载中...",
font=("SimHei", 10, "italic"),
fg="#666"
)
loading_label.grid(row=self.content_frame.grid_size()[1], column=0, sticky="ew", pady=10)
# 短暂延迟,让用户看到加载提示
self.plot_window.after(50, lambda: self._create_charts(current_nets, loading_label))
def _create_charts(self, nets, loading_label):
"""创建图表并移除加载提示"""
# 移除加载提示
loading_label.destroy()
# 创建图表
for net in nets:
self.create_net_chart(net)
self.loaded_count += len(nets)
self.loading = False
# 更新状态
self.status_label.config(text=f"已加载 {self.loaded_count} 个NET,共 {len(self.net_list)} 个")
# 检查是否全部加载完成
if self.loaded_count >= len(self.net_list):
self.status_label.config(text=f"全部加载完成,共 {len(self.net_list)} 个NET", fg="green")
def create_net_chart(self, net):
"""为单个NET创建图表"""
net_data = self.net_data_dict.get(net, pd.DataFrame())
if net_data.empty:
return
# 获取当前内容框架中的行数
row_idx = self.content_frame.grid_size()[1]
# 创建图表框架
frame = tk.Frame(self.content_frame, bd=2, relief=tk.GROOVE, padx=10, pady=10)
frame.grid(row=row_idx, column=0, sticky="nsew", padx=5, pady=5)
# 添加NET标签
tk.Label(frame, text=f"NET: {net}", font=("SimHei", 12, "bold")).pack(anchor=tk.W)
# 创建图表
fig = Figure(figsize=(10, 4), dpi=100)
ax = fig.add_subplot(111)
# 准备数据
long_data = net_data.melt(var_name="Lot", value_name="测量值")
long_data["批次序号"] = long_data["Lot"].str.extract(r"(\d+)").astype(int)
long_data = long_data.sort_values("批次序号")
# 绘制箱线图
sns.boxplot(
x="Lot",
y="测量值",
data=long_data,
ax=ax,
boxprops=dict(facecolor="lightblue", alpha=0.7),
medianprops=dict(color="red", linewidth=2),
)
# 设置图表样式
ax.set_title(f"{net} 各批次数据分布", fontsize=12)
ax.set_xlabel("Lot", fontsize=10)
ax.set_ylabel("测量值", fontsize=10)
ax.tick_params(axis="x", labelsize=8, rotation=30)
ax.grid(axis="y", linestyle="--", alpha=0.7)
fig.tight_layout()
# 标注统计值
all_values = long_data["测量值"].dropna()
if not all_values.empty:
data_range = all_values.max() - all_values.min()
for i, col in enumerate(net_data.columns):
col_values = net_data[col].dropna()
if col_values.empty:
continue
q1 = col_values.quantile(0.25)
median = col_values.median()
q3 = col_values.quantile(0.75)
text_offset = data_range * 0.03
ax.text(i, q1 - text_offset, f"Q1: {q1:.2f}", ha="center", fontsize=8)
ax.text(
i,
median,
f"Med: {median:.2f}",
ha="center",
fontsize=8,
color="red",
)
ax.text(i, q3 + text_offset, f"Q3: {q3:.2f}", ha="center", fontsize=8)
# 嵌入图表到Tkinter
canvas_plot = FigureCanvasTkAgg(fig, master=frame)
canvas_plot.draw()
canvas_plot.get_tk_widget().pack(fill=tk.X)
# 添加工具栏
toolbar = NavigationToolbar2Tk(canvas_plot, frame)
toolbar.update()
canvas_plot.get_tk_widget().pack(fill=tk.X)
def update_display(self):
"""根据当前视图模式更新显示"""
# 清除当前内容
for widget in self.content_frame.winfo_children():
widget.destroy()
# 重置无限滚动计数器
self.loaded_count = 0
if self.view_mode == "pagination":
# 分页模式
page_idx = self.current_page
start_idx = page_idx * self.per_page
end_idx = min((page_idx + 1) * self.per_page, len(self.net_list))
current_nets = self.net_list[start_idx:end_idx]
# 更新标题和页码标签
self.plot_window.title(f"批次数据分布箱线图 ({page_idx + 1}/{self.total_pages})")
self.page_label.config(text=f"{page_idx + 1}/{self.total_pages}")
# 更新按钮状态
self.prev_btn.config(state=tk.NORMAL if page_idx > 0 else tk.DISABLED)
self.next_btn.config(state=tk.NORMAL if page_idx < self.total_pages - 1 else tk.DISABLED)
# 创建当前页的图表
for net in current_nets:
self.create_net_chart(net)
# 更新状态
self.status_label.config(text=f"显示 {start_idx+1}-{end_idx} 个NET,共 {len(self.net_list)} 个")
else:
# 无限滚动模式
self.plot_window.title("批次数据分布箱线图 (无限滚动模式)")
self.status_label.config(text=f"已加载 0 个NET,共 {len(self.net_list)} 个")
# 初始加载第一组图表
self.load_more()
def process_batches():
input_text = batch_entry.get().strip()
if not input_text:
messagebox.showwarning("输入警告", "请输入批次号后再处理!")
return
batch_list = re.split(r"[,\s]+", input_text)
batch_list = [b for b in batch_list if b]
if not batch_list:
messagebox.showwarning("输入警告", "未识别到有效批次号!")
return
# 解析NET输入
net_text = net_entry.get().strip()
net_list = []
if net_text:
raw_nets = re.split(r"[,\s]+", net_text)
raw_nets = [n.strip() for n in raw_nets if n.strip()]
for net in raw_nets:
if net.startswith("net") and net[3:].isdigit():
net_list.append(net)
elif net.isdigit():
net_list.append(f"net{net}")
else:
print(f"警告:无效NET格式 '{net}',已忽略")
net_list = list(dict.fromkeys(net_list))
net_list.sort(key=lambda x: int(x[3:]))
print(f"已解析NET列表(排序后):{', '.join(net_list)}")
else:
# 默认显示前20个NET
net_list = [f"net{i}" for i in range(1, 21)]
print("未指定NET编号,将显示前20个NET")
process_btn.config(state=tk.DISABLED)
log_text.delete(1.0, tk.END)
print(f"开始处理批次:{', '.join(batch_list)}\n")
# 收集所有批次的数据
batch_data_dict = collect_batch_data(batch_list)
# 如果没有找到任何批次数据
if not batch_data_dict:
messagebox.showerror("错误", "未找到任何批次数据!")
process_btn.config(state=tk.NORMAL)
return
# 如果用户没有指定NET,使用第一个批次的所有NET列
if not net_list:
first_batch = next(iter(batch_data_dict.values()))
net_list = [col for col in first_batch.columns if col.startswith("net")]
net_list.sort(key=lambda x: int(x[3:]))
print(f"自动获取NET列表: {', '.join(net_list[:20])} (显示前20个)")
# 创建NET图表查看器
NetPlotViewer(root, batch_data_dict, batch_list, net_list)
process_btn.config(state=tk.NORMAL)
print("所有批次处理完成!")
def create_gui():
global root, batch_entry, log_text, process_btn, net_entry
root = tk.Tk()
root.title("電測阻值箱線圖")
root.geometry("800x650")
root.resizable(True, True)
# 批次号输入区域
input_frame = tk.Frame(root, padx=10, pady=10)
input_frame.pack(fill=tk.X)
tk.Label(
input_frame, text="批次号输入(多个批次用逗号/空格分隔):", font=("SimHei", 10)
).pack(side=tk.LEFT)
batch_entry = tk.Entry(input_frame, width=50, font=("SimHei", 10))
batch_entry.pack(side=tk.LEFT, padx=5)
batch_entry.insert(0, "25AL90070800,25AL90070400")
# NET编号输入
net_frame = tk.Frame(root, padx=10, pady=5)
net_frame.pack(fill=tk.X)
tk.Label(
net_frame,
text="NET编号输入(多个用逗号/空格分隔,留空显示全部):",
font=("SimHei", 10),
).pack(side=tk.LEFT)
net_entry = tk.Entry(net_frame, width=50, font=("SimHei", 10))
net_entry.pack(side=tk.LEFT, padx=5)
net_entry.insert(0, "1,2,3,4,5") # 測試先默認
# 按钮
btn_frame = tk.Frame(root, padx=10, pady=5)
btn_frame.pack(fill=tk.X)
process_btn = tk.Button(
btn_frame,
text="开始处理",
command=process_batches,
bg="#4CAF50",
fg="white",
font=("SimHei", 10),
)
process_btn.pack(side=tk.LEFT, padx=5)
clear_btn = tk.Button(
btn_frame,
text="清空日志",
command=lambda: log_text.delete(1.0, tk.END),
bg="#f44336",
fg="white",
font=("SimHei", 10),
)
clear_btn.pack(side=tk.LEFT, padx=5)
# 日志
log_frame = tk.Frame(root, padx=10, pady=5)
log_frame.pack(fill=tk.BOTH, expand=True)
tk.Label(log_frame, text="处理日志:", font=("SimHei", 10)).pack(anchor=tk.W)
log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, font=("SimHei", 9))
log_text.pack(fill=tk.BOTH, expand=True)
sys.stdout = RedirectText(log_text)
root.mainloop()
if __name__ == "__main__":
create_gui()
我現在想要先把數據收集完成後,在進行輸入net,畫圖,這樣如果我要更換查看net,就不用再重新執行程式重新收集。只要這個界面不關閉,收集一次後,可以多次輸入net進行查看
最新发布