图解scrollbar

由于博客内容为空,暂无法提供包含关键信息的摘要。
 
import pandas as pd import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Polygon from matplotlib.ticker import MaxNLocator import tkinter as tk from tkinter import filedialog, messagebox, ttk import os from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # 设置中文字体 plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"] plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 class PetrologyPlotter: """岩石学数据处理与绘图类""" def __init__(self, diagram_type="ACF", normalize=True): """ 初始化岩石学绘图器 参数: diagram_type: 图解类型,可选"ACF"或"A'KF" normalize: 是否进行归一化处理 """ self.diagram_type = diagram_type self.normalize = normalize self.data = None self.processed_data = None self.plot_data = None # 存储投点数据 # 氧化物的摩尔质量 (g/mol) self.molar_masses = { 'SiO2': 60.08, 'TiO2': 79.87, 'Al2O3': 101.96, 'Fe2O3': 159.69, 'FeO': 71.85, 'MnO': 70.94, 'MgO': 40.30, 'CaO': 56.08, 'Na2O': 61.98, 'K2O': 94.20, 'P2O5': 141.94, 'H2O': 18.02, 'CO2': 44.01, 'SO3': 80.06 } # 三角形顶点坐标 self.vertices = np.array([ [0.0, 0.0], # 左下角 (C/F) [1.0, 0.0], # 右下角 (F/C) [0.5, 0.866] # 顶部 (A/A') ]) # 投点相关属性 self.points = [] # 存储投点坐标 self.point_labels = [] # 存储投点标签 self.point_markers = [] # 存储投点标记对象 self.fig = None self.ax = None self.canvas = None # 边的定义(顶点索引) self.edges = [ (0, 1), # 底边 (1, 2), # 右边 (2, 0) # 左边 ] def read_data(self, file_path): """读取Excel数据""" try: self.data = pd.read_excel(file_path) print(f"成功读取数据,包含 {len(self.data)} 个样本") # 获取第一列作为样本名 first_column = self.data.columns[0] self.data['样本名'] = self.data[first_column].copy() # 处理空值,替换为"Sample"加上行号 self.data['样本名'] = self.data['样本名'].fillna('') for i, row in self.data.iterrows(): if not row['样本名']: self.data.at[i, '样本名'] = f'Sample_{i + 1}' return True except Exception as e: print(f"读取数据时出错: {e}") messagebox.showerror("错误", f"读取数据时出错: {e}") return False def process_data(self): """处理数据,计算ACF或A'KF值""" if self.data is None: print("请先读取数据") messagebox.showwarning("警告", "请先读取数据") return False # 确保必要的氧化物列存在 required_oxides = ['SiO2', 'TiO2', 'Al2O3', 'Fe2O3', 'FeO', 'MnO', 'MgO', 'CaO', 'Na2O', 'K2O', 'P2O5'] # 检查并处理缺失的氧化物列 missing_oxides = [oxide for oxide in required_oxides if oxide not in self.data.columns] if missing_oxides: print(f"警告: 缺少氧化物列 {', '.join(missing_oxides)},将使用0填充") for oxide in missing_oxides: self.data[oxide] = 0.0 # 计算摩尔数 molar_data = pd.DataFrame() for oxide in required_oxides: if oxide in self.data.columns: molar_data[oxide] = self.data[oxide] / self.molar_masses[oxide] # 计算阳离子数 (假设所有Fe为FeO) cations = pd.DataFrame() cations['Si'] = molar_data['SiO2'] cations['Ti'] = molar_data['TiO2'] cations['Al'] = molar_data['Al2O3'] * 2 cations['Fe3+'] = molar_data['Fe2O3'] * 2 cations['Fe2+'] = molar_data['FeO'] cations['Mn'] = molar_data['MnO'] cations['Mg'] = molar_data['MgO'] cations['Ca'] = molar_data['CaO'] cations['Na'] = molar_data['Na2O'] * 2 cations['K'] = molar_data['K2O'] * 2 cations['P'] = molar_data['P2O5'] * 2 # 计算总阳离子数 total_cations = cations.sum(axis=1) # 阳离子归一化到8个Oxygen (标准矿物计算) oxygen_factor = 8 / (cations['Si'] * 2 + cations['Ti'] * 2 + cations['Al'] * 1.5 + cations['Fe3+'] * 1.5 + cations['Fe2+'] + cations['Mn'] + cations['Mg'] + cations['Ca'] + cations['Na'] * 0.5 + cations['K'] * 0.5 + cations['P'] * 2.5) normalized_cations = cations.multiply(oxygen_factor, axis=0) # 计算ACF或A'KF值 if self.diagram_type == "ACF": # A = Al2O3 + Fe2O3 - (Na2O + K2O) # C = CaO # F = FeO + MgO + MnO self.processed_data = pd.DataFrame() self.processed_data['A'] = normalized_cations['Al'] + normalized_cations['Fe3+'] - ( normalized_cations['Na'] + normalized_cations['K']) self.processed_data['C'] = normalized_cations['Ca'] self.processed_data['F'] = normalized_cations['Fe2+'] + normalized_cations['Mg'] + normalized_cations['Mn'] # 确保没有负值 self.processed_data[self.processed_data < 0] = 0 # 归一化处理 if self.normalize: total = self.processed_data.sum(axis=1) self.processed_data['A'] = self.processed_data['A'] / total * 100 self.processed_data['C'] = self.processed_data['C'] / total * 100 self.processed_data['F'] = self.processed_data['F'] / total * 100 # 检查总和是否接近100 self.processed_data['sum'] = self.processed_data['A'] + self.processed_data['C'] + self.processed_data['F'] self.processed_data['valid'] = np.abs(self.processed_data['sum'] - 100) < 1e-6 print( f"处理完成: {sum(self.processed_data['valid'])}个有效样本, {sum(~self.processed_data['valid'])}个无效样本") elif self.diagram_type == "A'KF": # A' = Al2O3 - (Na2O + K2O) # K = K2O # F = FeO + MgO + MnO self.processed_data = pd.DataFrame() self.processed_data["A'"] = normalized_cations['Al'] - (normalized_cations['Na'] + normalized_cations['K']) self.processed_data['K'] = normalized_cations['K'] self.processed_data['F'] = normalized_cations['Fe2+'] + normalized_cations['Mg'] + normalized_cations['Mn'] # 确保没有负值 self.processed_data[self.processed_data < 0] = 0 # 归一化处理 if self.normalize: total = self.processed_data.sum(axis=1) self.processed_data["A'"] = self.processed_data["A'"] / total * 100 self.processed_data['K'] = self.processed_data['K'] / total * 100 self.processed_data['F'] = self.processed_data['F'] / total * 100 # 检查总和是否接近100 self.processed_data['sum'] = self.processed_data["A'"] + self.processed_data['K'] + self.processed_data['F'] self.processed_data['valid'] = np.abs(self.processed_data['sum'] - 100) < 1e-6 print( f"处理完成: {sum(self.processed_data['valid'])}个有效样本, {sum(~self.processed_data['valid'])}个无效样本") # 添加样本名列 self.processed_data['样本名'] = self.data['样本名'] return True def ternary_to_cartesian(self, a, b, c): """ 将三元坐标转换为笛卡尔坐标 参数: a, b, c: 三元坐标值 返回: x, y: 对应的笛卡尔坐标 """ # 顶点坐标 A = self.vertices[2] # 顶部 B = self.vertices[0] # 左下角 C = self.vertices[1] # 右下角 # 归一化 total = a + b + c a_norm = a / total b_norm = b / total c_norm = c / total # 计算笛卡尔坐标 x = a_norm * A[0] + b_norm * B[0] + c_norm * C[0] y = a_norm * A[1] + b_norm * B[1] + c_norm * C[1] return x, y def cartesian_to_ternary(self, x, y): """ 将笛卡尔坐标转换为三元坐标 参数: x, y: 笛卡尔坐标值 返回: a, b, c: 对应的三元坐标值 """ # 三角形顶点坐标 A = self.vertices[2] # 顶部 B = self.vertices[0] # 左下角 C = self.vertices[1] # 右下角 # 计算三角形面积 total_area = 0.5 * abs(A[0] * (B[1] - C[1]) + B[0] * (C[1] - A[1]) + C[0] * (A[1] - B[1])) # 计算三个子三角形面积 area_A = 0.5 * abs(x * (B[1] - C[1]) + B[0] * (C[1] - y) + C[0] * (y - B[1])) area_B = 0.5 * abs(A[0] * (y - C[1]) + x * (C[1] - A[1]) + C[0] * (A[1] - y)) area_C = 0.5 * abs(A[0] * (B[1] - y) + B[0] * (y - A[1]) + x * (A[1] - B[1])) # 计算三元坐标 a = area_A / total_area b = area_B / total_area c = area_C / total_area return a, b, c def plot_diagram(self, title=None, show_labels=True, show_grid=True, show_legend=True, selected_samples=None): """ 绘制ACF或A'KF三角图或联合图 参数: title: 图表标题 show_labels: 是否显示标签 show_grid: 是否显示网格线 show_legend: 是否显示图例 selected_samples: 要显示的样本索引列表,如果为None则显示所有样本 """ if self.processed_data is None: print("请先处理数据") messagebox.showwarning("警告", "请先处理数据") return None # 创建图形 self.fig, self.ax = plt.subplots(figsize=(10, 9)) # 绘制联合图 if self.diagram_type == "联合图": # ACF三角形(正三角) acf_vertices = np.array([ [0.0, 0.0], # 左下角 (C) [1.0, 0.0], # 右下角 (F) [0.5, 0.866] # 顶部 (A) ]) acf_triangle = Polygon(acf_vertices, fill=False, edgecolor='black', linewidth=2) self.ax.add_patch(acf_triangle) # A'KF三角形(倒三角) akf_vertices = np.array([ [0.0, 0.866], # 左上角 (A') [1.0, 0.866], # 右上角 (K) [0.5, 0.0] # 底部 (F) ]) akf_triangle = Polygon(akf_vertices, fill=False, edgecolor='black', linewidth=2) self.ax.add_patch(akf_triangle) # 标记顶点 acf_vertex_labels = ['C', 'F', 'A'] for i, (x, y) in enumerate(acf_vertices): self.ax.text(x, y, acf_vertex_labels[i], fontsize=14, fontweight='bold', ha='center', va='center', bbox=dict(facecolor='white', alpha=0.7, pad=5)) akf_vertex_labels = ["A'", 'K', 'F'] for i, (x, y) in enumerate(akf_vertices): self.ax.text(x, y, akf_vertex_labels[i], fontsize=14, fontweight='bold', ha='center', va='center', bbox=dict(facecolor='white', alpha=0.7, pad=5)) # 绘制刻度线和标签 if show_labels: self._add_scale_labels(self.ax, acf_vertices, "ACF") self._add_scale_labels(self.ax, akf_vertices, "A'KF") # 绘制网格线 if show_grid: self._add_grid_lines(self.ax, acf_vertices) self._add_grid_lines(self.ax, akf_vertices) # 如果有处理好的数据,绘制数据点 if self.processed_data is not None: # 确定要显示的样本 if selected_samples is None: samples_to_plot = self.processed_data else: samples_to_plot = self.processed_data.iloc[selected_samples] # 为不同样本定义不同颜色 colors = plt.cm.rainbow(np.linspace(0, 1, len(samples_to_plot))) # 转换数据点为笛卡尔坐标并绘制 for i, (index, row) in enumerate(samples_to_plot.iterrows()): # ACF投点 acf_x, acf_y = self.ternary_to_cartesian(row['A'], row['C'], row['F']) label = row['样本名'] if '样本名' in row else f"样本 {index + 1}" self.ax.scatter(acf_x, acf_y, c=[colors[i]], s=60, alpha=0.8, label=f"ACF_{label}") # A'KF投点 akf_x, akf_y = self.ternary_to_cartesian(row["A'"], row['K'], row['F']) # 修正A'KF投点的y坐标 akf_y = (1 - akf_y) * 0.866 self.ax.scatter(akf_x, akf_y, c=[colors[i]], s=60, alpha=0.8, label=f"A'KF_{label}") else: # 单独的ACF或A'KF图 # 绘制三角形边界 triangle = Polygon(self.vertices, fill=False, edgecolor='black', linewidth=2) self.ax.add_patch(triangle) # 设置坐标轴范围 self.ax.set_xlim(-0.1, 1.1) self.ax.set_ylim(-0.1, 1.0) # 隐藏坐标轴 self.ax.set_axis_off() # 添加标题 if title is None: title = f"{self.diagram_type} 图解" self.ax.set_title(title, fontsize=16, pad=20) # 标记顶点 if self.diagram_type == "ACF": vertex_labels = ['C', 'F', 'A'] else: # A'KF vertex_labels = ['F', 'K', "A'"] for i, (x, y) in enumerate(self.vertices): self.ax.text(x, y, vertex_labels[i], fontsize=14, fontweight='bold', ha='center', va='center', bbox=dict(facecolor='white', alpha=0.7, pad=5)) # 绘制刻度线和标签 if show_labels: self._add_scale_labels(self.ax) # 绘制网格线 if show_grid: self._add_grid_lines(self.ax) # 如果有处理好的数据,绘制数据点 if self.processed_data is not None: # 确定要显示的样本 if selected_samples is None: samples_to_plot = self.processed_data else: samples_to_plot = self.processed_data.iloc[selected_samples] # 为不同样本定义不同颜色 colors = plt.cm.rainbow(np.linspace(0, 1, len(samples_to_plot))) # 转换数据点为笛卡尔坐标并绘制 for i, (index, row) in enumerate(samples_to_plot.iterrows()): if self.diagram_type == "ACF": x, y = self.ternary_to_cartesian(row['A'], row['C'], row['F']) else: # A'KF x, y = self.ternary_to_cartesian(row["A'"], row['K'], row['F']) # 绘制数据点 label = row['样本名'] if '样本名' in row else f"样本 {index + 1}" self.ax.scatter(x, y, c=[colors[i]], s=60, alpha=0.8, label=label) # 添加图例 if show_legend and not samples_to_plot.empty: self.ax.legend(loc='upper right', frameon=True, framealpha=0.8) # 调整布局 plt.tight_layout() return self.fig, self.ax def auto_plot_points(self, sample_indices=None): """ 自动投点 参数: sample_indices: 要投点的样本索引列表,如果为None则只投第一个样本 """ if self.processed_data is None or len(self.processed_data) == 0: print("没有处理好的数据") return # 检查 self.ax 是否为 None,如果是则先绘制图表 if self.ax is None: self.plot_diagram() # 清除之前的投点 self.points = [] self.point_labels = [] if hasattr(self, 'point_markers'): for marker in self.point_markers: if marker in self.ax.collections: marker.remove() self.point_markers = [] # 确定要投点的样本 if sample_indices is None or len(sample_indices) == 0: sample_indices = [0] # 默认投第一个样本 # 为不同样本定义不同颜色 colors = plt.cm.rainbow(np.linspace(0, 1, len(sample_indices))) # 为每个样本投点 for sample_idx, color in zip(sample_indices, colors): if sample_idx >= len(self.processed_data): print(f"样本索引 {sample_idx} 超出范围") continue sample = self.processed_data.iloc[sample_idx] sample_name = sample['样本名'] if '样本名' in sample else f"样本 {sample_idx + 1}" if self.diagram_type == "ACF": a_value = sample['A'] c_value = sample['C'] f_value = sample['F'] # 在C-F边上投C点(从F到C,值从0到100) c_x = (100 - c_value) / 100 * self.vertices[1][0] + c_value / 100 * self.vertices[0][0] c_y = (100 - c_value) / 100 * self.vertices[1][1] + c_value / 100 * self.vertices[0][1] self.points.append((c_x, c_y)) self.point_labels.append(f"C: {c_value:.1f}") # 在F-A边上投F点(从F到A,值从0到100) f_x = (100 - f_value) / 100 * self.vertices[1][0] + f_value / 100 * self.vertices[2][0] f_y = (100 - f_value) / 100 * self.vertices[1][1] + f_value / 100 * self.vertices[2][1] self.points.append((f_x, f_y)) self.point_labels.append(f"F: {f_value:.1f}") # 在A-C边上投A点(从A到C,值从0到100) a_x = (100 - a_value) / 100 * self.vertices[2][0] + a_value / 100 * self.vertices[0][0] a_y = (100 - a_value) / 100 * self.vertices[2][1] + a_value / 100 * self.vertices[0][1] self.points.append((a_x, a_y)) self.point_labels.append(f"A: {a_value:.1f}") # 绘制投点 point_labels = ['C点', 'F点', 'A点'] for i, ((x, y), label, marker_label) in enumerate( zip(self.points[-3:], point_labels, self.point_labels[-3:])): full_label = f"{sample_name}_{label}" marker = self.ax.scatter(x, y, c=[color], s=80, alpha=0.8, zorder=5) self.point_markers.append(marker) self.ax.text(x + 0.03, y + 0.03, f"{marker_label}", fontsize=10, ha='left', va='bottom', bbox=dict(facecolor='white', alpha=0.7, pad=2)) # 绘制投点结果 result_x, result_y = self.ternary_to_cartesian(a_value, c_value, f_value) result_marker = self.ax.scatter(result_x, result_y, c=[color], s=120, alpha=0.8, zorder=6, marker='*', label=f"{sample_name}投点结果") # 添加结果标签 label = f"A: {a_value:.1f}, C: {c_value:.1f}, F: {f_value:.1f}" self.ax.text(result_x + 0.05, result_y + 0.05, label, fontsize=10, ha='left', va='bottom', bbox=dict(facecolor='white', alpha=0.7, pad=3)) elif self.diagram_type == "A'KF": a_prime_value = sample["A'"] k_value = sample['K'] f_value = sample['F'] # 在F-K边上投F点(从F到K,值从0到100) f_x = (100 - f_value) / 100 * self.vertices[0][0] + f_value / 100 * self.vertices[1][0] f_y = (100 - f_value) / 100 * self.vertices[0][1] + f_value / 100 * self.vertices[1][1] self.points.append((f_x, f_y)) self.point_labels.append(f"F: {f_value:.1f}") # 在K-A'边上投K点(从K到A',值从0到100) k_x = (100 - k_value) / 100 * self.vertices[1][0] + k_value / 100 * self.vertices[2][0] k_y = (100 - k_value) / 100 * self.vertices[1][1] + k_value / 100 * self.vertices[2][1] self.points.append((k_x, k_y)) self.point_labels.append(f"K: {k_value:.1f}") # 在A'-F边上投A'点(从A'到F,值从0到100) a_prime_x = (100 - a_prime_value) / 100 * self.vertices[2][0] + a_prime_value / 100 * self.vertices[0][ 0] a_prime_y = (100 - a_prime_value) / 100 * self.vertices[2][1] + a_prime_value / 100 * self.vertices[0][ 1] self.points.append((a_prime_x, a_prime_y)) self.point_labels.append(f"A': {a_prime_value:.1f}") # 绘制投点 point_labels = ['F点', 'K点', "A'点"] for i, ((x, y), label, marker_label) in enumerate( zip(self.points[-3:], point_labels, self.point_labels[-3:])): full_label = f"{sample_name}_{label}" marker = self.ax.scatter(x, y, c=[color], s=80, alpha=0.8, zorder=5) self.point_markers.append(marker) self.ax.text(x + 0.03, y + 0.03, f"{marker_label}", fontsize=10, ha='left', va='bottom', bbox=dict(facecolor='white', alpha=0.7, pad=2)) # 绘制投点结果 result_x, result_y = self.ternary_to_cartesian(a_prime_value, k_value, f_value) result_marker = self.ax.scatter(result_x, result_y + 0.866, c=[color], s=120, alpha=0.8, zorder=6, marker='*', label=f"{sample_name}_A'KF投点结果") # 添加结果标签 label = f"A': {a_prime_value:.1f}, K: {k_value:.1f}, F: {f_value:.1f}" self.ax.text(result_x + 0.05, result_y + 0.866 + 0.05, label, fontsize=10, ha='left', va='bottom', bbox=dict(facecolor='white', alpha=0.7, pad=3)) def manual_plot_point(self, a, b, c): """手动投点""" if self.ax is None: print("请先绘制图解") return if self.diagram_type == "ACF": x, y = self.ternary_to_cartesian(a, b, c) label = f"A: {a:.1f}, C: {b:.1f}, F: {c:.1f}" elif self.diagram_type == "A'KF": x, y = self.ternary_to_cartesian(a, b, c) if self.diagram_type == "联合图": y += 0.866 label = f"A': {a:.1f}, K: {b:.1f}, F: {c:.1f}" marker = self.ax.scatter(x, y, c='red', s=120, alpha=0.8, zorder=6, marker='*') self.point_markers.append(marker) self.points.append((x, y)) self.point_labels.append(label) self.ax.text(x + 0.05, y + 0.05, label, fontsize=10, ha='left', va='bottom', bbox=dict(facecolor='white', alpha=0.7, pad=3)) self.fig.canvas.draw_idle() def cancel_last_point(self): """取消最后一个投点""" if self.point_markers: marker = self.point_markers.pop() marker.remove() self.points.pop() self.point_labels.pop() self.fig.canvas.draw_idle() def cancel_diagram(self): """取消图解""" if self.fig: plt.close(self.fig) self.fig = None self.ax = None self.points = [] self.point_labels = [] self.point_markers = [] if self.canvas: self.canvas.get_tk_widget().destroy() self.canvas = None def clear_diagram(self): """清除图解""" if self.ax: self.ax.clear() self.points = [] self.point_labels = [] self.point_markers = [] self.plot_diagram(title=self.title, show_labels=True, show_grid=True, show_legend=True) self.fig.canvas.draw_idle() def _add_scale_labels(self, ax, vertices=None, diagram_type=None): """添加三角图的刻度和标签""" if vertices is None: vertices = self.vertices # 定义刻度位置 ticks = np.linspace(0, 1, 11) # 左侧边 for t in ticks: x = (1 - t) * vertices[0][0] + t * vertices[2][0] y = (1 - t) * vertices[0][1] + t * vertices[2][1] ax.plot([x], [y], 'k.', markersize=3) if t < 1: # 不在顶点重复标注 ax.text(x - 0.03, y - 0.02, f"{int(t * 100)}", fontsize=9, ha='right') # 右侧边 for t in ticks: x = (1 - t) * vertices[1][0] + t * vertices[2][0] y = (1 - t) * vertices[1][1] + t * vertices[2][1] ax.plot([x], [y], 'k.', markersize=3) if t < 1: # 不在顶点重复标注 ax.text(x + 0.03, y - 0.02, f"{int(t * 100)}", fontsize=9, ha='left') # 底边 for t in ticks: x = (1 - t) * vertices[0][0] + t * vertices[1][0] y = (1 - t) * vertices[0][1] + t * vertices[1][1] ax.plot([x], [y], 'k.', markersize=3) if 0 < t < 1: # 不在顶点重复标注 if diagram_type == "A'KF": ax.text(x, y + 0.04, f"{int((1 - t) * 100)}", fontsize=9, ha='center') else: ax.text(x, y - 0.04, f"{int((1 - t) * 100)}", fontsize=9, ha='center') def _add_grid_lines(self, ax, vertices=None): """添加三角图的网格线""" if vertices is None: vertices = self.vertices # 定义网格线的数量 n_lines = 10 # 绘制平行于左侧边的网格线 for i in range(1, n_lines): t = i / n_lines # 从右侧边到左边的线 x1 = (1 - t) * vertices[1][0] + t * vertices[2][0] y1 = (1 - t) * vertices[1][1] + t * vertices[2][1] x2 = (1 - t) * vertices[1][0] + t * vertices[0][0] y2 = (1 - t) * vertices[1][1] + t * vertices[0][1] ax.plot([x1, x2], [y1, y2], 'gray', linestyle='--', linewidth=0.5) # 绘制平行于右侧边的网格线 for i in range(1, n_lines): t = i / n_lines # 从左侧边到底边的线 x1 = (1 - t) * vertices[0][0] + t * vertices[2][0] y1 = (1 - t) * vertices[0][1] + t * vertices[2][1] x2 = (1 - t) * vertices[0][0] + t * vertices[1][0] y2 = (1 - t) * vertices[0][1] + t * vertices[1][1] ax.plot([x1, x2], [y1, y2], 'gray', linestyle='--', linewidth=0.5) # 绘制平行于底边的网格线 for i in range(1, n_lines): t = i / n_lines # 从左侧边到右侧边的线 x1 = (1 - t) * vertices[0][0] + t * vertices[2][0] y1 = (1 - t) * vertices[0][1] + t * vertices[2][1] x2 = (1 - t) * vertices[1][0] + t * vertices[2][0] y2 = (1 - t) * vertices[1][1] + t * vertices[2][1] ax.plot([x1, x2], [y1, y2], 'gray', linestyle='--', linewidth=0.5) class ScrollableFrame(ttk.Frame): """可滚动的框架""" def __init__(self, container, *args, **kwargs): super().__init__(container, *args, **kwargs) # 创建一个垂直滚动条 self.vscrollbar = ttk.Scrollbar(self, orient="vertical") self.vscrollbar.pack(side="right", fill="y") # 创建一个水平滚动条 self.hscrollbar = ttk.Scrollbar(self, orient="horizontal") self.hscrollbar.pack(side="bottom", fill="x") # 创建一个画布 self.canvas = tk.Canvas(self, yscrollcommand=self.vscrollbar.set, xscrollcommand=self.hscrollbar.set) self.canvas.pack(side="left", fill="both", expand=True) # 配置滚动条 self.vscrollbar.config(command=self.canvas.yview) self.hscrollbar.config(command=self.canvas.xview) # 创建一个框架来放置内容 self.content_frame = ttk.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("<Configure>", self._on_canvas_configure) # 支持鼠标滚轮滚动 self.canvas.bind_all("<MouseWheel>", self._on_mousewheel) def _on_frame_configure(self, event): """当内容框架大小改变时,更新画布的滚动区域""" self.canvas.configure(scrollregion=self.canvas.bbox("all")) def _on_canvas_configure(self, event): """当画布大小改变时,调整内容框架的宽度""" self.canvas.itemconfig(self.canvas_frame, width=event.width) def _on_mousewheel(self, event): """处理鼠标滚轮事件""" if event.state & 0x1: # Shift键被按下,水平滚动 self.canvas.xview_scroll(int(-1 * (event.delta / 120)), "units") else: # 垂直滚动 self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") class PetrologyApp: """岩石学图解分析应用程序""" def __init__(self, root): self.root = root self.root.title("岩石学ACF和A'KF图解分析工具") self.root.geometry("1000x700") self.root.resizable(True, True) # 设置窗口图标 try: self.root.iconbitmap("petrology.ico") except: pass # 忽略图标设置失败的情况 # 创建绘图器实例 self.plotter = None self.file_path = None self.diagram_type = tk.StringVar(value="ACF") self.normalize = tk.BooleanVar(value=True) self.title = tk.StringVar(value="") # 样本选择相关变量 self.sample_listbox = None self.sample_vars = [] self.sample_checkboxes = [] # 手动投点输入框 self.a_entry = None self.b_entry = None self.c_entry = None # 创建界面 self.create_widgets() def create_widgets(self): """创建GUI组件""" # 创建主框架 main_frame = ttk.Frame(self.root, padding="20") main_frame.pack(fill=tk.BOTH, expand=True) # 左侧控制面板(使用可滚动框架) control_frame = ttk.LabelFrame(main_frame, text="控制面板", padding="5") control_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10) # 创建可滚动框架 self.scrollable_frame = ScrollableFrame(control_frame) self.scrollable_frame.pack(fill=tk.BOTH, expand=True) # 所有控件都添加到可滚动框架的content_frame中 content_frame = self.scrollable_frame.content_frame # 文件选择部分 file_frame = ttk.LabelFrame(content_frame, text="文件选择", padding="10") file_frame.pack(fill=tk.X, pady=10) ttk.Label(file_frame, text="Excel文件:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) self.file_entry = ttk.Entry(file_frame, width=30) self.file_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5, pady=5) ttk.Button(file_frame, text="浏览...", command=self.browse_file).grid(row=0, column=2, padx=5, pady=5) file_frame.columnconfigure(1, weight=1) # 设置部分 settings_frame = ttk.LabelFrame(content_frame, text="图解设置", padding="10") settings_frame.pack(fill=tk.X, pady=10) ttk.Label(settings_frame, text="图解类型:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) ttk.Radiobutton(settings_frame, text="ACF", variable=self.diagram_type, value="ACF").grid( row=0, column=1, sticky=tk.W, padx=5, pady=5) ttk.Radiobutton(settings_frame, text="A'KF", variable=self.diagram_type, value="A'KF").grid( row=0, column=2, sticky=tk.W, padx=5, pady=5) ttk.Radiobutton(settings_frame, text="联合图", variable=self.diagram_type, value="联合图").grid( row=0, column=3, sticky=tk.W, padx=5, pady=5) ttk.Checkbutton(settings_frame, text="归一化处理", variable=self.normalize).grid( row=1, column=0, sticky=tk.W, padx=5, pady=5) ttk.Label(settings_frame, text="图表标题:").grid(row=2, column=0, sticky=tk.W, padx=5, pady=5) ttk.Entry(settings_frame, textvariable=self.title, width=30).grid( row=2, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=5) settings_frame.columnconfigure(1, weight=1) # 样本选择部分 sample_frame = ttk.LabelFrame(content_frame, text="样本选择", padding="10") sample_frame.pack(fill=tk.BOTH, expand=True, pady=10) ttk.Label(sample_frame, text="已加载样本:").pack(anchor=tk.W, padx=5, pady=5) # 创建样本列表容器 self.sample_frame = ttk.Frame(sample_frame) self.sample_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 滚动条 scrollbar = ttk.Scrollbar(self.sample_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 样本列表框 self.sample_listbox = tk.Listbox(self.sample_frame, selectmode=tk.MULTIPLE, yscrollcommand=scrollbar.set, height=10) self.sample_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.config(command=self.sample_listbox.yview) # 选择按钮 select_frame = ttk.Frame(sample_frame) select_frame.pack(fill=tk.X, pady=5) ttk.Button(select_frame, text="全选", command=self.select_all_samples).pack(side=tk.LEFT, padx=5) ttk.Button(select_frame, text="取消全选", command=self.deselect_all_samples).pack(side=tk.LEFT, padx=5) # 手动投点部分 manual_point_frame = ttk.LabelFrame(content_frame, text="手动投点", padding="10") manual_point_frame.pack(fill=tk.X, pady=10) if self.diagram_type.get() == "ACF": ttk.Label(manual_point_frame, text="A值:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) ttk.Label(manual_point_frame, text="C值:").grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) ttk.Label(manual_point_frame, text="F值:").grid(row=0, column=2, sticky=tk.W, padx=5, pady=5) elif self.diagram_type.get() == "A'KF": ttk.Label(manual_point_frame, text="A'值:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=5) ttk.Label(manual_point_frame, text="K值:").grid(row=0, column=1, sticky=tk.W, padx=5, pady=5) ttk.Label(manual_point_frame, text="F值:").grid(row=0, column=2, sticky=tk.W, padx=5, pady=5) self.a_entry = ttk.Entry(manual_point_frame, width=10) self.a_entry.grid(row=1, column=0, padx=5, pady=5) self.b_entry = ttk.Entry(manual_point_frame, width=10) self.b_entry.grid(row=1, column=1, padx=5, pady=5) self.c_entry = ttk.Entry(manual_point_frame, width=10) self.c_entry.grid(row=1, column=2, padx=5, pady=5) ttk.Button(manual_point_frame, text="手动投点", command=self.manual_plot_point).grid(row=1, column=3, padx=5, pady=5) # 按钮部分 button_frame = ttk.Frame(content_frame) button_frame.pack(fill=tk.X, pady=20) # 使用grid布局美化按钮排放 ttk.Button(button_frame, text="加载数据", command=self.load_data).grid(row=0, column=0, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="处理数据", command=self.process_data).grid(row=0, column=1, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="绘制图解", command=self.plot_diagram).grid(row=0, column=2, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="自动投点", command=self.auto_plot_points).grid(row=1, column=0, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="保存图解", command=self.save_diagram).grid(row=1, column=1, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="清空投点", command=self.clear_points).grid(row=1, column=2, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="取消该投点", command=self.cancel_last_point).grid(row=2, column=0, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="取消图解", command=self.cancel_diagram).grid(row=2, column=1, padx=5, pady=5, sticky="ew") ttk.Button(button_frame, text="清除图解", command=self.clear_diagram).grid(row=2, column=2, padx=5,pady=5, sticky="ew") button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(1, weight=1) button_frame.columnconfigure(2, weight=1) # 投点结果部分 self.pointing_results = tk.Text(content_frame, height=10, width=40) self.pointing_results.pack(fill=tk.BOTH, expand=True, padx=5, pady=10) self.pointing_results.insert(tk.END, "投点结果将显示在这里...") self.pointing_results.config(state=tk.DISABLED) # 右侧图表显示区域 self.plot_frame = ttk.LabelFrame(main_frame, text="图解显示", padding="10") self.plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10) # 状态部分 self.status_var = tk.StringVar(value="请选择Excel文件并加载数据") ttk.Label(self.root, textvariable=self.status_var, anchor=tk.W).pack(fill=tk.X, side=tk.BOTTOM, pady=10) def browse_file(self): """浏览并选择Excel文件""" file_path = filedialog.askopenfilename( filetypes=[("Excel files", "*.xlsx *.xls")] ) if file_path: self.file_path = file_path self.file_entry.delete(0, tk.END) self.file_entry.insert(0, os.path.basename(file_path)) self.status_var.set(f"已选择文件: {os.path.basename(file_path)}") def load_data(self): """加载数据""" if not self.file_path: messagebox.showwarning("警告", "请先选择Excel文件") return # 创建绘图器实例 self.plotter = PetrologyPlotter( diagram_type=self.diagram_type.get(), normalize=self.normalize.get() ) # 读取数据 if self.plotter.read_data(self.file_path): self.status_var.set(f"已成功加载数据,共{len(self.plotter.data)}个样本") # 更新样本列表 self.sample_listbox.delete(0, tk.END) for i, row in self.plotter.data.iterrows(): sample_name = row.get('样本名', f"样本_{i + 1}") self.sample_listbox.insert(tk.END, sample_name) # 启用处理数据按钮 # 假设这里有一个处理数据按钮的引用 # self.process_button.config(state=tk.NORMAL) def process_data(self): """处理数据""" if self.plotter is None: messagebox.showwarning("警告", "请先加载数据") return # 更新绘图器的图解类型和归一化设置 self.plotter.diagram_type = self.diagram_type.get() self.plotter.normalize = self.normalize.get() # 处理数据 if self.plotter.process_data(): valid_count = sum(self.plotter.processed_data['valid']) invalid_count = sum(~self.plotter.processed_data['valid']) self.status_var.set(f"数据处理完成,有效样本: {valid_count}, 无效样本: {invalid_count}") def plot_diagram(self): """绘制图解""" if self.plotter is None or self.plotter.processed_data is None: messagebox.showwarning("警告", "请先处理数据") return # 获取选中的样本 selected_indices = self.sample_listbox.curselection() title = self.title.get() if self.title.get() else None fig, ax = self.plotter.plot_diagram(title=title, selected_samples=selected_indices) if fig and ax: if self.plotter.canvas: self.plotter.canvas.get_tk_widget().destroy() self.plotter.canvas = FigureCanvasTkAgg(fig, master=self.plot_frame) self.plotter.canvas.draw() self.plotter.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) def auto_plot_points(self): """自动投点""" if self.plotter is None or self.plotter.processed_data is None: messagebox.showwarning("警告", "请先处理数据") return selected_indices = self.sample_listbox.curselection() self.plotter.auto_plot_points(sample_indices=selected_indices) self.plotter.fig.canvas.draw_idle() def manual_plot_point(self): """手动投点""" if self.plotter is None or self.plotter.ax is None: messagebox.showwarning("警告", "请先绘制图解") return try: a = float(self.a_entry.get()) b = float(self.b_entry.get()) c = float(self.c_entry.get()) self.plotter.manual_plot_point(a, b, c) except ValueError: messagebox.showerror("错误", "请输入有效的数值") def cancel_last_point(self): """取消最后一个投点""" if self.plotter: self.plotter.cancel_last_point() def cancel_diagram(self): """取消图解""" if self.plotter: self.plotter.cancel_diagram() def clear_diagram(self): """清除图解""" if self.plotter: self.plotter.clear_diagram() def clear_points(self): """清空投点""" if self.plotter: self.plotter.points = [] self.plotter.point_labels = [] if hasattr(self.plotter, 'point_markers'): for marker in self.plotter.point_markers: if marker in self.plotter.ax.collections: marker.remove() self.plotter.point_markers = [] self.plotter.fig.canvas.draw_idle() def select_all_samples(self): """全选样本""" self.sample_listbox.select_set(0, tk.END) def deselect_all_samples(self): """取消全选样本""" self.sample_listbox.selection_clear(0, tk.END) def save_diagram(self): """保存图解""" if self.plotter and self.plotter.fig: file_path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG files", "*.png"), ("PDF files", "*.pdf")]) if file_path: self.plotter.fig.savefig(file_path, dpi=300) messagebox.showinfo("提示", f"图解已保存到 {file_path}") if __name__ == "__main__": root = tk.Tk() app = PetrologyApp(root) root.mainloop()优化一下改代码,并进行调试
06-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值