Grid - Double Header Columns - Ext JS

本文讨论了一个被认为是新网格特性的话题。作者表示已经进行了搜索但未能找到相关信息,并询问是否有了解该特性的读者可以提供更多信息。
I beleive I read this was afeature of the new grid. I've looked around and I haven't found anything.

Anyone in the know?
Reply With Quote
import os import re import sys import time import threading import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext from tkinter.font import Font import fnmatch import subprocess import shutil import docx from openpyxl import load_workbook import PyPDF2 import zipfile import chardet import xlrd # 添加对旧版Excel的支持 class FileSearchApp: def __init__(self, master): self.master = master master.title("高级文件搜索工具") master.geometry("1200x800") master.minsize(900, 650) # 设置现代主题 self.style = ttk.Style() self.style.theme_use("vista" if sys.platform == "win32" else "aqua") # 创建主框架 main_frame = ttk.Frame(master, padding=10) main_frame.pack(fill=tk.BOTH, expand=True) # 创建左侧搜索面板 search_frame = ttk.LabelFrame(main_frame, text="搜索选项", padding=10) search_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10), pady=5) # 使用网格布局管理器 row = 0 ttk.Label(search_frame, text="搜索目录:").grid(row=row, column=0, sticky="w", pady=5) self.dir_entry = ttk.Entry(search_frame, width=40) self.dir_entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") self.dir_entry.insert(0, os.getcwd()) ttk.Button(search_frame, text="浏览...", command=self.browse_directory).grid(row=row, column=2, padx=5, pady=5) row += 1 ttk.Label(search_frame, text="关键词:").grid(row=row, column=0, sticky="w", pady=5) self.keyword_entry = ttk.Entry(search_frame, width=40) self.keyword_entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") row += 1 ttk.Label(search_frame, text="文件过滤:").grid(row=row, column=0, sticky="w", pady=5) self.filter_entry = ttk.Entry(search_frame, width=40) self.filter_entry.grid(row=row, column=1, padx=5, pady=5, sticky="we") self.filter_entry.insert(0, "*") row += 1 # 添加分隔线 ttk.Separator(search_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=3, sticky="ew", pady=10) row += 1 # 搜索选项 options_frame = ttk.Frame(search_frame) options_frame.grid(row=row, column=0, columnspan=3, sticky="we", padx=5, pady=5) # 使用网格布局替代pack布局,更紧凑 self.case_var = tk.BooleanVar(value=False) ttk.Checkbutton(options_frame, text="忽略大小写", variable=self.case_var).grid(row=0, column=0, sticky="w", padx=(0, 10)) self.regex_var = tk.BooleanVar(value=False) ttk.Checkbutton(options_frame, text="正则表达式", variable=self.regex_var).grid(row=0, column=1, sticky="w", padx=(0, 10)) self.binary_var = tk.BooleanVar(value=False) self.binary_check = ttk.Checkbutton(options_frame, text="包含二进制", variable=self.binary_var) self.binary_check.grid(row=0, column=2, sticky="w") # 添加文件大小限制选项 self.limit_var = tk.BooleanVar(value=True) ttk.Checkbutton(options_frame, text="限制大小(100MB)", variable=self.limit_var).grid(row=0, column=3, sticky="w", padx=(10, 0)) row += 1 # 添加分隔线 ttk.Separator(search_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=3, sticky="ew", pady=10) row += 1 # 搜索按钮 button_frame = ttk.Frame(search_frame) button_frame.grid(row=row, column=0, columnspan=3, pady=10) self.search_button = ttk.Button(button_frame, text="开始搜索", command=self.start_search) self.search_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button(button_frame, text="停止搜索", command=self.stop_search, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=5) self.export_button = ttk.Button(button_frame, text="导出结果", command=self.export_results) self.export_button.pack(side=tk.LEFT, padx=5) row += 1 # 添加分隔线 ttk.Separator(search_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=3, sticky="ew", pady=10) row += 1 # 状态栏 status_frame = ttk.Frame(search_frame) status_frame.grid(row=row, column=0, columnspan=3, sticky="we", pady=5) # 状态标签(左对齐) self.status_label = ttk.Label(status_frame, text="就绪", font=("Arial", 9)) self.status_label.pack(side=tk.LEFT, anchor='w') # 进度条(中间,可伸缩) self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar( status_frame, variable=self.progress_var, length=200, mode='determinate' ) self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10) # 结果统计(右对齐) self.stats_label = ttk.Label(status_frame, text="", font=("Arial", 9)) self.stats_label.pack(side=tk.RIGHT) # 创建结果面板 results_frame = ttk.LabelFrame(main_frame, text="搜索结果", padding=10) results_frame.pack(fill=tk.BOTH, expand=True, padx=(5, 0), pady=5) # 分割窗格 paned_window = ttk.PanedWindow(results_frame, orient=tk.HORIZONTAL) paned_window.pack(fill=tk.BOTH, expand=True) # 左侧文件列表 file_list_frame = ttk.Frame(paned_window) paned_window.add(file_list_frame, weight=1) # 使用Treeview替代Listbox columns = ("filename", "path") self.file_tree = ttk.Treeview(file_list_frame, columns=columns, show="headings", selectmode="browse") # 设置列标题 self.file_tree.heading("filename", text="文件名") self.file_tree.heading("path", text="路径") # 设置列宽 self.file_tree.column("filename", width=200, anchor="w") self.file_tree.column("path", width=300, anchor="w") self.file_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.file_tree.bind('<<TreeviewSelect>>', self.show_file_content) self.file_tree.bind('<Double-1>', self.open_selected_file) file_scroll = ttk.Scrollbar(file_list_frame, command=self.file_tree.yview) file_scroll.pack(side=tk.RIGHT, fill=tk.Y) self.file_tree.config(yscrollcommand=file_scroll.set) # 右键菜单 self.file_menu = tk.Menu(self.master, tearoff=0) self.file_menu.add_command(label="打开文件", command=self.open_selected_file) self.file_menu.add_command(label="打开文件位置", command=self.open_file_location) self.file_tree.bind("<Button-3>", self.show_file_context_menu) # 右侧文件内容预览 content_frame = ttk.Frame(paned_window) paned_window.add(content_frame, weight=2) self.content_text = scrolledtext.ScrolledText( content_frame, wrap=tk.WORD, font=("Consolas", 10), padx=5, pady=5 ) self.content_text.pack(fill=tk.BOTH, expand=True) # 文本区域右键菜单 text_menu = tk.Menu(self.master, tearoff=0) text_menu.add_command(label="复制", command=self.copy_selected_text) self.content_text.bind("<Button-3>", lambda e: text_menu.tk_popup(e.x_root, e.y_root)) # 高亮标签 self.content_text.tag_configure("match", background="yellow") self.content_text.tag_configure("linenum", foreground="blue") self.content_text.tag_configure("header", foreground="darkgreen", font=("Arial", 10, "bold")) self.content_text.tag_configure("warning", foreground="red", font=("Arial", 10, "italic")) # 初始化变量 self.results = {} self.all_files = [] self.file_paths = [] self.stop_requested = False self.search_thread = None def browse_directory(self): directory = filedialog.askdirectory(title="选择搜索目录") if directory: self.dir_entry.delete(0, tk.END) self.dir_entry.insert(0, directory) def start_search(self): # 重置状态 self.progress_var.set(0) self.stop_requested = False self.results = {} self.all_files = [] self.file_paths = [] self.file_tree.delete(*self.file_tree.get_children()) self.content_text.delete(1.0, tk.END) self.status_label.config(text="正在搜索...") self.search_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.stats_label.config(text="") # 获取搜索参数 directory = self.dir_entry.get().strip() keyword = self.keyword_entry.get().strip() file_filter = self.filter_entry.get().strip() # 验证输入 if not directory or not os.path.isdir(directory): messagebox.showerror("错误", "请选择有效的搜索目录") self.search_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return if not keyword: messagebox.showerror("错误", "请输入搜索关键词") self.search_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return # 解析文件过滤器 if file_filter == "": filter_patterns = ['*'] else: separators = [';', '|', ' ', ','] for sep in separators: if sep in file_filter: filter_patterns = [pat.strip() for pat in file_filter.split(sep)] break else: filter_patterns = [file_filter] # 编译搜索模式 flags = re.IGNORECASE if self.case_var.get() else 0 try: if self.regex_var.get(): pattern = re.compile(keyword, flags) else: escaped_keyword = re.escape(keyword) pattern = re.compile(escaped_keyword, flags) except re.error as e: messagebox.showerror("正则表达式错误", f"无效的正则表达式: {str(e)}") self.search_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return # 在后台线程中执行搜索 self.search_thread = threading.Thread( target=self.perform_search, args=(directory, filter_patterns, pattern), daemon=True ) self.search_thread.start() def perform_search(self, directory, filter_patterns, pattern): """在后台线程中执行文件搜索""" try: # 收集所有匹配的文件 all_files = [] for root, _, files in os.walk(directory): if self.stop_requested: self.master.after(0, lambda: self.status_label.config(text="搜索已取消")) return for file in files: file_path = os.path.join(root, file) # 检查文件大小限制(避免处理超大文件) try: file_size = os.path.getsize(file_path) if file_size > 100 * 1024 * 1024: # 100MB continue except: continue # 检查是否符合任一过滤模式 if any(fnmatch.fnmatch(file, pat) for pat in filter_patterns): all_files.append(file_path) self.all_files = all_files total_files = len(all_files) # 初始化进度条 self.master.after(0, lambda: self.progress_bar.config(maximum=total_files)) self.master.after(0, lambda: self.stats_label.config(text=f"扫描到 {total_files} 个文件")) # 搜索每个文件 self.results = {} processed = 0 matches_found = 0 for file_path in self.all_files: if self.stop_requested: break processed += 1 # 更新进度条(安全方式) self.master.after(0, lambda v=processed: self.progress_var.set(v)) if processed % 10 == 0: # 每处理10个文件更新一次进度 self.master.after(0, lambda p=processed, t=total_files: self.stats_label.config(text=f"处理中: {p}/{t} 文件 ({round(p/t*100,1)}%)")) # 忽略二进制文件(除非用户选择包含) if not self.binary_var.get() and self.is_binary(file_path): continue # 获取文件扩展名 _, ext = os.path.splitext(file_path) ext_lower = ext.lower() # 处理Office文档 if ext_lower in ['.docx', '.xlsx', '.xls', '.xlsm', '.pptx', '.pdf', '.doc']: matches = self.search_in_office_file(file_path, pattern) # 处理压缩文件 elif ext_lower in ['.zip', '.rar', '.7z', '.tar', '.gz']: matches = self.search_in_archive(file_path, pattern) # 处理文本文件 else: matches = self.search_in_text_file(file_path, pattern) if matches: self.results[file_path] = matches matches_found += len(matches) # 在UI线程中添加文件到列表 filename = os.path.basename(file_path) self.master.after(0, lambda fp=file_path, fn=filename: self.file_tree.insert("", "end", values=(fn, fp))) # 更新完成状态 if self.stop_requested: status_text = f"搜索已取消 - 找到 {len(self.results)} 个文件, {matches_found} 个匹配项" else: status_text = f"搜索完成 - 找到 {len(self.results)} 个文件, {matches_found} 个匹配项" self.master.after(0, lambda: self.status_label.config(text=status_text)) self.master.after(0, lambda: self.stats_label.config(text=f"已处理 {processed}/{total_files} 文件")) self.master.after(0, lambda: self.progress_var.set(total_files)) except Exception as e: # 记录详细错误日志 error_info = f"搜索错误: {type(e).__name__} - {str(e)}" print(error_info) with open("search_errors.log", "a") as log: log.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {error_info}\n") import traceback traceback.print_exc(file=log) self.master.after(0, lambda: messagebox.showerror( "搜索错误", f"发生严重错误: {error_info}\n详细信息已记录到日志" )) finally: self.master.after(0, lambda: self.search_button.config(state=tk.NORMAL)) self.master.after(0, lambda: self.stop_button.config(state=tk.DISABLED)) self.search_thread = None def search_in_text_file(self, filepath, pattern): """在文本文件中搜索匹配项""" matches = [] try: encoding = self.detect_encoding(filepath) try: with open(filepath, 'r', encoding=encoding, errors='replace') as f: for line_num, line in enumerate(f, 1): if pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except UnicodeDecodeError: # 特殊编码处理回退 with open(filepath, 'rb') as f: content = f.read() try: text = content.decode('utf-8', errors='replace') except: text = content.decode('latin-1', errors='replace') for line_num, line in enumerate(text.splitlines(), 1): if pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except Exception as e: print(f"读取文本文件失败 {filepath}: {str(e)}") return matches def search_in_office_file(self, filepath, pattern): """在Office文件中搜索文本内容""" matches = [] _, ext = os.path.splitext(filepath) ext_lower = ext.lower() try: # DOCX文件处理 if ext_lower == '.docx': doc = docx.Document(filepath) # 搜索段落 for i, para in enumerate(doc.paragraphs, 1): if para.text and pattern.search(para.text): matches.append((i, f"段落 {i}: {para.text[:100]}" + ("..." if len(para.text) > 100 else ""))) # 搜索表格 for table in doc.tables: for row_idx, row in enumerate(table.rows, 1): for cell_idx, cell in enumerate(row.cells, 1): if cell.text and pattern.search(cell.text): content = cell.text.strip() if len(content) > 100: content = content[:100] + "..." matches.append((row_idx, f"表格 行{row_idx}列{cell_idx}: {content}")) # XLSX/XLS文件处理 elif ext_lower in ('.xlsx', '.xls', '.xlsm'): # 处理新格式Excel文件 if ext_lower in ('.xlsx', '.xlsm'): wb = load_workbook(filepath, read_only=True, data_only=True) for sheet_name in wb.sheetnames: sheet = wb[sheet_name] for row_idx, row in enumerate(sheet.iter_rows(values_only=True), 1): for col_idx, cell in enumerate(row, 1): if cell is not None and pattern.search(str(cell)): cell_ref = f"{chr(64+col_idx)}{row_idx}" cell_value = str(cell).strip() if len(cell_value) > 100: cell_value = cell_value[:100] + "..." matches.append((row_idx, f"工作表 '{sheet_name}' 单元格 {cell_ref}: {cell_value}")) # 处理旧格式Excel文件 elif ext_lower == '.xls': wb = xlrd.open_workbook(filepath) for sheet_idx in range(wb.nsheets): sheet = wb.sheet_by_index(sheet_idx) for row_idx in range(sheet.nrows): for col_idx in range(sheet.ncols): cell = sheet.cell_value(row_idx, col_idx) if cell and pattern.search(str(cell)): cell_ref = f"{chr(65+col_idx)}{row_idx+1}" cell_value = str(cell).strip() if len(cell_value) > 100: cell_value = cell_value[:100] + "..." matches.append((row_idx+1, f"工作表 '{sheet.name}' 单元格 {cell_ref}: {cell_value}")) # PPTX文件处理 elif ext_lower == '.pptx': from pptx import Presentation ppt = Presentation(filepath) # 搜索幻灯片文本 for slide_idx, slide in enumerate(ppt.slides, 1): for shape in slide.shapes: if hasattr(shape, "text"): if shape.text and pattern.search(shape.text): content = shape.text.strip() if len(content) > 100: content = content[:100] + "..." matches.append((slide_idx, f"幻灯片 {slide_idx}: {content}")) # PDF文件处理 elif ext_lower == '.pdf': with open(filepath, 'rb') as f: pdf = PyPDF2.PdfReader(f) for page_num in range(len(pdf.pages)): page_text = pdf.pages[page_num].extract_text() if page_text and pattern.search(page_text): # 提取匹配内容 matches_found = [] for match in pattern.finditer(page_text): context = page_text[max(0, match.start()-20):match.end()+20] context = context.replace('\n', ' ').strip() matches_found.append(context) # 添加到结果 if matches_found: preview = "; ".join(matches_found[:3]) # 显示前3个匹配 if len(matches_found) > 3: preview += f" ... (+{len(matches_found)-3} 更多)" matches.append((page_num+1, f"页面 {page_num+1}: {preview}")) # 旧版DOC文件处理 elif ext_lower == '.doc': try: # 尝试使用antiword转换DOC为文本 result = subprocess.run(['antiword', filepath], capture_output=True, text=True, timeout=10) if result.returncode == 0: doc_text = result.stdout for line_num, line in enumerate(doc_text.split('\n'), 1): if line and pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except Exception: # 备用方法:使用python-doc处理 import win32com.client word = win32com.client.Dispatch("Word.Application") word.Visible = False doc = word.Documents.Open(filepath) doc_text = doc.Content.Text doc.Close() word.Quit() for line_num, line in enumerate(doc_text.split('\n'), 1): if line and pattern.search(line): cleaned_line = line.strip() if len(cleaned_line) > 150: cleaned_line = cleaned_line[:150] + "..." matches.append((line_num, cleaned_line)) except Exception as e: print(f"处理Office文件失败 {filepath}: {str(e)}") return matches def search_in_archive(self, filepath, pattern): """在压缩文件中搜索匹配项""" matches = [] _, ext = os.path.splitext(filepath) ext_lower = ext.lower() try: # ZIP文件处理 if ext_lower in ('.zip', '.jar', '.war'): with zipfile.ZipFile(filepath, 'r') as archive: for name in archive.namelist(): # 只处理文本文件和Office文档 if not name.endswith(('/')) and not self.is_binary(name): try: with archive.open(name) as file: content = file.read(4096) # 只读取前4KB # 尝试检测编码 result = chardet.detect(content) encoding = result['encoding'] if result['confidence'] > 0.7 else 'utf-8' # 解码内容并搜索 try: text_content = content.decode(encoding, errors='replace') if pattern.search(text_content): matches.append((name, f"压缩文件中的文件: {name}")) except: # 二进制内容搜索 if pattern.search(content): matches.append((name, f"压缩文件中的文件(二进制内容): {name}")) except Exception: continue # 其他压缩格式(需要外部工具) elif ext_lower in ('.rar', '.7z', '.tar', '.gz'): # 使用7zip命令行工具解压并搜索 temp_dir = tempfile.mkdtemp() try: subprocess.run(['7z', 'x', filepath, f'-o{temp_dir}'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, timeout=60) # 递归搜索解压的目录 for root, _, files in os.walk(temp_dir): for file in files: full_path = os.path.join(root, file) _, file_ext = os.path.splitext(file) file_ext = file_ext.lower() # 只在文本/Office文件中搜索 if file_ext in ['', '.txt', '.py', '.java', '.c', '.cpp', '.h', '.html', '.xml', '.json', '.csv', '.docx', '.xlsx', '.pptx', '.pdf']: if file_ext in ['.docx', '.xlsx', '.pptx', '.pdf']: file_matches = self.search_in_office_file(full_path, pattern) else: file_matches = self.search_in_text_file(full_path, pattern) if file_matches: matches.append((file, f"压缩文件中的文件: {file}")) finally: shutil.rmtree(temp_dir, ignore_errors=True) except Exception as e: print(f"处理压缩文件失败 {filepath}: {str(e)}") return matches def detect_encoding(self, filepath): """改进的文件编码检测方法""" try: # 尝试读取文件前4KB进行编码检测 with open(filepath, 'rb') as f: raw_data = f.read(4096) # 使用chardet进行编码检测 result = chardet.detect(raw_data) # 优先使用检测到的编码,否则尝试常见编码 if result['confidence'] > 0.7: return result['encoding'] # 中文环境常用编码回退策略 common_encodings = ['utf-8', 'gbk', 'gb2312', 'gb18030', 'latin1'] for encoding in common_encodings: try: # 尝试解码验证 raw_data.decode(encoding, errors='strict') return encoding except UnicodeDecodeError: continue # 默认使用UTF-8 return 'utf-8' except Exception: return 'utf-8' def is_binary(self, filepath): """检查文件是否为二进制文件""" try: with open(filepath, 'rb') as f: chunk = f.read(1024) if b'\0' in chunk: # 空字节是二进制文件的标志 return True # 检查高字节值 if any(byte >= 0x80 for byte in chunk): return True return False except: return False def stop_search(self): """停止当前搜索""" self.stop_requested = True self.status_label.config(text="正在停止搜索...") self.stop_button.config(state=tk.DISABLED) # 冻结进度条显示当前进度 self.progress_bar.config(mode='indeterminate' if self.progress_var.get() == 0 else 'determinate') def export_results(self): """导出搜索结果""" if not self.results: messagebox.showinfo("导出结果", "没有可导出的搜索结果") return file_path = filedialog.asksaveasfilename( title="保存搜索结果为", defaultextension=".csv", filetypes=[("CSV 文件", "*.csv"), ("文本文件", "*.txt")] ) if not file_path: return try: with open(file_path, 'w', encoding='utf-8') as f: # 写出CSV头部 f.write("文件路径,匹配行号,匹配内容\n") # 写出每项结果 for file, matches in self.results.items(): for line_num, match_content in matches: # 清理内容中的逗号 cleaned_content = match_content.replace('"', '""').replace(',', ';') f.write(f'"{file}",{line_num},"{cleaned_content}"\n') messagebox.showinfo("导出成功", f"搜索结果已保存到:\n{file_path}") except Exception as e: messagebox.showerror("导出错误", f"导出失败: {str(e)}") def show_file_content(self, event=None): """在预览区域显示文件内容""" # 获取选中的文件 selection = self.file_tree.selection() if not selection: return selected_item = selection[0] filepath = self.file_tree.item(selected_item, 'values')[1] # 清空预览区域 self.content_text.delete(1.0, tk.END) # 获取文件扩展名 _, ext = os.path.splitext(filepath) ext_lower = ext.lower() # 显示文件路径标题 self.content_text.insert(tk.END, f"文件路径: {filepath}\n", "header") # 处理不同文件类型 try: # 处理Office文档 if ext_lower in ['.docx', '.xlsx', '.xls', '.xlsm', '.pptx', '.pdf', '.doc']: matches = self.results.get(filepath, []) if not matches: self.content_text.insert(tk.END, "\n未找到匹配内容\n", "warning") return self.content_text.insert(tk.END, f"\n找到 {len(matches)} 个匹配项:\n\n", "header") # 显示每个匹配项 for i, (line_num, content) in enumerate(matches, 1): self.content_text.insert(tk.END, f"[匹配项 {i}] 位置: {line_num}\n") self.content_text.insert(tk.END, f"{content}\n\n") # 处理压缩文件 elif ext_lower in ['.zip', '.rar', '.7z', '.tar', '.gz']: matches = self.results.get(filepath, []) if not matches: self.content_text.insert(tk.END, "\n未找到匹配内容\n", "warning") return self.content_text.insert(tk.END, f"\n找到 {len(matches)} 个匹配项:\n\n", "header") for i, (file_in_zip, content) in enumerate(matches, 1): self.content_text.insert(tk.END, f"[匹配项 {i}] 文件: {file_in_zip}\n") self.content_text.insert(tk.END, f"{content}\n\n") # 处理文本文件 else: # 获取关键词高亮模式 keyword = self.keyword_entry.get().strip() flags = re.IGNORECASE if self.case_var.get() else 0 if self.regex_var.get(): try: pattern = re.compile(keyword, flags) except: pattern = None else: pattern = re.compile(re.escape(keyword), flags) # 显示文件内容并高亮匹配 self.content_text.insert(tk.END, "\n文件内容:\n\n", "header") # 限制预览内容大小(最多显示1000行) max_preview_lines = 1000 try: encoding = self.detect_encoding(filepath) with open(filepath, 'r', encoding=encoding, errors='replace') as f: line_count = 0 for line in f: line_count += 1 if line_count > max_preview_lines: self.content_text.insert(tk.END, f"\n... (文件过大,仅显示前{max_preview_lines}行)\n", "warning") break # 插入行号 self.content_text.insert(tk.END, f"{line_count:4d} | ", "linenum") # 插入行内容并高亮匹配 if pattern: start_idx = 0 for match in pattern.finditer(line): # 插入匹配前的文本 self.content_text.insert(tk.END, line[start_idx:match.start()]) # 插入高亮的匹配文本 self.content_text.insert(tk.END, match.group(), "match") start_idx = match.end() # 插入匹配后的文本 self.content_text.insert(tk.END, line[start_idx:]) else: self.content_text.insert(tk.END, line) except UnicodeDecodeError: self.content_text.insert(tk.END, "\n无法解码此文件内容(可能是二进制文件)\n", "warning") except Exception as e: self.content_text.insert(tk.END, f"\n读取文件时出错: {str(e)}\n", "warning") except Exception as e: self.content_text.insert(tk.END, f"\n加载文件内容出错: {str(e)}\n", "warning") def open_selected_file(self, event=None): """用系统默认程序打开选中的文件""" selection = self.file_tree.selection() if not selection: return selected_item = selection[0] filepath = self.file_tree.item(selected_item, 'values')[1] try: if sys.platform == "win32": os.startfile(filepath) elif sys.platform == "darwin": # macOS subprocess.run(["open", filepath]) else: # Linux subprocess.run(["xdg-open", filepath]) except Exception as e: messagebox.showerror("打开文件失败", f"无法打开文件: {str(e)}") def open_file_location(self): """在文件资源管理器中打开文件所在位置""" selection = self.file_tree.selection() if not selection: return selected_item = selection[0] filepath = self.file_tree.item(selected_item, 'values')[1] folder = os.path.dirname(filepath) try: if sys.platform == "win32": subprocess.run(["explorer", "/select,", filepath]) elif sys.platform == "darwin": # macOS subprocess.run(["open", "-R", filepath]) else: # Linux subprocess.run(["xdg-open", folder]) except Exception as e: messagebox.showerror("打开位置失败", f"无法打开位置: {str(e)}") def show_file_context_menu(self, event): """显示文件列表的右键菜单""" item = self.file_tree.identify_row(event.y) if item: self.file_tree.selection_set(item) self.file_menu.tk_popup(event.x_root, event.y_root) def copy_selected_text(self): """复制预览区域中选中的文本""" selected_text = self.content_text.get(tk.SEL_FIRST, tk.SEL_LAST) if selected_text: self.master.clipboard_clear() self.master.clipboard_append(selected_text) # 程序入口 if __name__ == "__main__": root = tk.Tk() app = FileSearchApp(root) # 添加图标(如果有) try: if sys.platform == "win32": root.iconbitmap("search_icon.ico") else: img = tk.PhotoImage(file="search_icon.png") root.iconphoto(True, img) except: pass root.mainloop() 这是我的代码,所有的功能都已经实现,但我觉得它太过于冗余,帮我优化一下,另外我觉得目前这个布局不够美观,也不够实用,帮我修改布局,另外,在搜索到关键字之后,我希望能够在文件预览那个窗口中,将搜索到的关键字进行高亮,或者可以选择高亮。去除搜索压缩文件的内容,因为我不需要搜索压缩文件,我大概搜索的文档类型分别是.c .h .prm .xlsx .xls .doc .docx .pdf
09-13
using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using System.IO.Ports; using System.IO; using System.Globalization; using System.Windows.Forms.DataVisualization.Charting; using OfficeOpenXml; using OfficeOpenXml.Style; using OfficeOpenXml.Drawing.Chart; using System.Diagnostics; using System.Reflection; using System.Text; using System.Linq; namespace PhysicsExperimentDisplay { [DataContract] public class ExperimentRecord { [DataMember] public string TimeStamp { get; set; } [DataMember] public double T1 { get; set; } [DataMember] public double T2 { get; set; } [DataMember] public double PlateWidth { get; set; } // 挡光片宽度 (m 或 mm,根据使用习惯) [DataMember] public double GateSpacing { get; set; } // 光电门间距 [DataMember] public double V1 { get; set; } [DataMember] public double V2 { get; set; } [DataMember] public double Acc { get; set; } } public partial class MainForm : Form { // 控件声明 private TableLayoutPanel mainLayout; private GroupBox gbSerial; private ComboBox cmbPorts; private ComboBox cmbBaudRate; private Button btnConnect; private Label lblPort; private Label lblBaud; private GroupBox gbData; private DataGridView dgvResults; private GroupBox gbChart; private Button btnExport; private Button btnClear; private SerialPort serialPort; private FlowLayoutPanel buttonPanel; private TableLayoutPanel serialLayout; private Chart chartPlot; // 新增控件:输入挡光片宽度和光电门间距、保存/加载历史 private Label lblPlateWidth; private TextBox txtPlateWidth; private Label lblGateSpacing; private TextBox txtGateSpacing; private Button btnSaveExperiment; private Button btnLoadExperiment; // 数据存储 private List<double> accelerationData = new List<double>(); private int dataPointCount = 0; private DateTime experimentStartTime = DateTime.Now; // 当前实验完整原始记录 private List<ExperimentRecord> currentRecords = new List<ExperimentRecord>(); // 保存目录 private readonly string experimentsFolder; public object JsonConvert { get; private set; } public object Newtonsoft { get; private set; } public MainForm() { // 初始化保存目录路径 experimentsFolder = Path.Combine(Application.StartupPath, "Experiments"); Directory.CreateDirectory(experimentsFolder); // 初始化组件 InitializeComponent(); // 初始化串口和图表 LoadAvailablePorts(); InitializePlot(); // 绑定按钮事件(确保事件处理器已实现) btnConnect.Click += btnConnect_Click; btnExport.Click += btnExport_Click; btnClear.Click += btnClear_Click; btnSaveExperiment.Click += btnSaveExperiment_Click; btnLoadExperiment.Click += btnLoadExperiment_Click; } private void btnLoadExperiment_Click(object sender, EventArgs e) { string selectedFile = null; OpenFileDialog ofd = null; try { // 在 try 内创建对话框,确保构造期抛出的异常也能被捕获 ofd = new System.Windows.Forms.OpenFileDialog(); ofd.InitialDirectory = experimentsFolder; ofd.Filter = "CSV 文件 (*.csv)|*.csv|JSON 文件 (*.json)|*.json|所有文件 (*.*)|*.*"; ofd.Title = "选择要加载的实验文件"; // 优先在标准 UI 环境中弹出系统对话框(传入父窗口) if (ofd.ShowDialog(this) == DialogResult.OK) { selectedFile = ofd.FileName; } else { return; } } catch (NotImplementedException nie) { // 记录诊断信息到日志文件,便于后续调查运行环境 try { string logPath = Path.Combine(experimentsFolder, "dialog_error.log"); var sbLog = new StringBuilder(); sbLog.AppendLine("=== OpenFileDialog NotImplementedException ==="); sbLog.AppendLine(DateTime.Now.ToString("o")); sbLog.AppendLine(nie.ToString()); try { sbLog.AppendLine($"OpenFileDialog runtime type: {ofd?.GetType().FullName ?? "null"}"); sbLog.AppendLine($"OpenFileDialog assembly: {ofd?.GetType().Assembly.FullName ?? "null"}"); } catch { /* 忽略获取类型信息失败 */ } try { sbLog.AppendLine($"Framework: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"); sbLog.AppendLine($"OS: {System.Runtime.InteropServices.RuntimeInformation.OSDescription}"); sbLog.AppendLine($"Is64BitProcess: {Environment.Is64BitProcess}"); } catch { /* 运行时信息获取失败则忽略 */ } File.AppendAllText(logPath, sbLog.ToString(), Encoding.UTF8); } catch { // 忽略日志写入错误,避免二次异常 } // 回退到内置选择器 selectedFile = PickFileFallback(); if (string.IsNullOrEmpty(selectedFile)) return; } catch (Exception ex) { MessageBox.Show($"打开文件对话框失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } finally { // 确保 Dispose,即使构造失败也安全 ofd?.Dispose(); } try { dgvResults.Rows.Clear(); accelerationData.Clear(); dataPointCount = 0; string ext = Path.GetExtension(selectedFile).ToLowerInvariant(); if (ext == ".csv") { var lines = File.ReadAllLines(selectedFile); // 假定第一行为表头:Time,T1,T2,V1,V2,Acc foreach (var line in lines.Skip(1)) { if (string.IsNullOrWhiteSpace(line)) continue; var cols = line.Split(','); int row = dgvResults.Rows.Add(); dgvResults.Rows[row].Cells[0].Value = cols.ElementAtOrDefault(0) ?? ""; dgvResults.Rows[row].Cells[1].Value = cols.ElementAtOrDefault(1) ?? ""; dgvResults.Rows[row].Cells[2].Value = cols.ElementAtOrDefault(2) ?? ""; dgvResults.Rows[row].Cells[3].Value = cols.ElementAtOrDefault(3) ?? ""; dgvResults.Rows[row].Cells[4].Value = cols.ElementAtOrDefault(4) ?? ""; dgvResults.Rows[row].Cells[5].Value = cols.ElementAtOrDefault(5) ?? ""; if (double.TryParse(cols.ElementAtOrDefault(5), NumberStyles.Any, CultureInfo.InvariantCulture, out double acc)) accelerationData.Add(acc); } } else if (ext == ".json") { var json = File.ReadAllText(selectedFile); var records = Newtonsoft.Json.JsonConvert.DeserializeObject<List<ExperimentRecord>>(json); if (records != null) { foreach (var r in records) { int row = dgvResults.Rows.Add(); dgvResults.Rows[row].Cells[0].Value = r.TimeStamp; dgvResults.Rows[row].Cells[1].Value = r.T1.ToString("0.000"); dgvResults.Rows[row].Cells[2].Value = r.T2.ToString("0.000"); dgvResults.Rows[row].Cells[3].Value = r.V1.ToString("0.000"); dgvResults.Rows[row].Cells[4].Value = r.V2.ToString("0.000"); dgvResults.Rows[row].Cells[5].Value = r.Acc.ToString("0.000"); accelerationData.Add(r.Acc); } } } dataPointCount = accelerationData.Count; // 用批量数据重建图表(避免每点重绘) chartPlot.Series["加速度"].Points.Clear(); for (int i = 0; i < accelerationData.Count; i++) chartPlot.Series["加速度"].Points.AddXY(i + 1, accelerationData[i]); chartPlot.Titles[0].Text = $"加速度变化曲线 (共 {dataPointCount} 个数据点)"; // 调整轴范围等(可复用 UpdatePlot 的逻辑) } catch (Exception ex) { MessageBox.Show($"加载失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // 简单回退文件选择器:在无法使用系统 OpenFileDialog 时使用(仅列出 experimentsFolder 下文件) private string PickFileFallback() { try { string[] files = Directory.GetFiles(experimentsFolder) .OrderByDescending(f => File.GetCreationTimeUtc(f)) .ToArray(); if (files.Length == 0) { MessageBox.Show("未找到历史实验文件。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return null; } using (Form pickForm = new Form()) { pickForm.Text = "选择要加载的实验文件(回退)"; pickForm.StartPosition = FormStartPosition.CenterParent; pickForm.ClientSize = new Size(600, 400); pickForm.FormBorderStyle = FormBorderStyle.FixedDialog; pickForm.MinimizeBox = false; pickForm.MaximizeBox = false; pickForm.ShowInTaskbar = false; var lb = new ListBox { Dock = DockStyle.Fill }; foreach (var f in files) lb.Items.Add(Path.GetFileName(f)); var btnPanel = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 40, FlowDirection = FlowDirection.RightToLeft, Padding = new Padding(6) }; var btnOk = new Button { Text = "确定", DialogResult = DialogResult.OK, AutoSize = true, Margin = new Padding(6) }; var btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, AutoSize = true, Margin = new Padding(6) }; btnPanel.Controls.Add(btnOk); btnPanel.Controls.Add(btnCancel); pickForm.Controls.Add(lb); pickForm.Controls.Add(btnPanel); // 双击快速选择 lb.DoubleClick += (s, e) => { if (lb.SelectedIndex >= 0) pickForm.DialogResult = DialogResult.OK; }; if (pickForm.ShowDialog() == DialogResult.OK && lb.SelectedIndex >= 0) { return files[lb.SelectedIndex]; } } } catch (Exception ex) { MessageBox.Show($"文件选择回退失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } return null; } private void btnSaveExperiment_Click(object sender, EventArgs e) { if (dgvResults.Rows.Count == 0) { MessageBox.Show("没有数据可保存。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } try { // 解析附加参数(挡光片宽度、光电门间距) double plateWidth = 0; double gateSpacing = 0; double.TryParse(txtPlateWidth?.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out plateWidth); double.TryParse(txtGateSpacing?.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out gateSpacing); // 从 DataGridView 构建记录列表 var records = new List<ExperimentRecord>(); foreach (DataGridViewRow row in dgvResults.Rows) { if (row.IsNewRow) continue; string timeStamp = row.Cells[0].Value?.ToString() ?? ""; double t1 = 0, t2 = 0, v1 = 0, v2 = 0, acc = 0; double.TryParse(row.Cells[1].Value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out t1); double.TryParse(row.Cells[2].Value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out t2); double.TryParse(row.Cells[3].Value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out v1); double.TryParse(row.Cells[4].Value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out v2); double.TryParse(row.Cells[5].Value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out acc); records.Add(new ExperimentRecord { TimeStamp = timeStamp, T1 = t1, T2 = t2, V1 = v1, V2 = v2, Acc = acc, PlateWidth = plateWidth, GateSpacing = gateSpacing }); } // 将当前记录缓存在内存 currentRecords = records; // 生成文件名(时间戳) string baseName = $"Experiment_{DateTime.Now:yyyyMMdd_HHmmss}"; string csvPath = Path.Combine(experimentsFolder, baseName + ".csv"); string jsonPath = Path.Combine(experimentsFolder, baseName + ".json"); // 写 CSV(带表头) var sb = new StringBuilder(); sb.AppendLine("TimeStamp,T1,T2,V1,V2,Acc,PlateWidth,GateSpacing"); foreach (var r in records) { string line = string.Join(",", EscapeCsv(r.TimeStamp), r.T1.ToString("0.###", CultureInfo.InvariantCulture), r.T2.ToString("0.###", CultureInfo.InvariantCulture), r.V1.ToString("0.###", CultureInfo.InvariantCulture), r.V2.ToString("0.###", CultureInfo.InvariantCulture), r.Acc.ToString("0.###", CultureInfo.InvariantCulture), r.PlateWidth.ToString("0.###", CultureInfo.InvariantCulture), r.GateSpacing.ToString("0.###", CultureInfo.InvariantCulture)); sb.AppendLine(line); } File.WriteAllText(csvPath, sb.ToString(), Encoding.UTF8); // 写 JSON(格式化) string json = JsonConvert.SerializeObject(records, Formatting.Indented); File.WriteAllText(jsonPath, json, Encoding.UTF8); MessageBox.Show($"保存成功:\n{csvPath}\n{jsonPath}", "保存完成", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"保存失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } // 本地函数:CSV 字段转义(简单处理逗号与引号) string EscapeCsv(string s) { if (string.IsNullOrEmpty(s)) return ""; if (s.Contains(",") || s.Contains("\"") || s.Contains("\n")) return $"\"{s.Replace("\"", "\"\"")}\""; return s; } } // 初始化UI private void InitializeComponent() { // 主窗体设置 this.SuspendLayout(); this.AutoScaleDimensions = new SizeF(8F, 15F); this.AutoScaleMode = AutoScaleMode.Font; this.ClientSize = new Size(1200, 800); this.Text = "气垫导轨实验数据监控"; // 创建主布局面板 this.mainLayout = new TableLayoutPanel { ColumnCount = 1, Dock = DockStyle.Fill, Padding = new Padding(10), RowCount = 4 }; mainLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 100F)); mainLayout.RowStyles.Add(new RowStyle(SizeType.Absolute, 70F)); // 增加高度以容纳输入控件 mainLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 40F)); mainLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 60F)); // 串口设置组 this.gbSerial = new GroupBox { Text = "串口设置", Dock = DockStyle.Fill }; // 串口布局 this.serialLayout = new TableLayoutPanel { ColumnCount = 4, Dock = DockStyle.Fill, RowCount = 2 }; serialLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 100F)); serialLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30F)); serialLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 100F)); serialLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30F)); serialLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); serialLayout.RowStyles.Add(new RowStyle(SizeType.Percent, 50F)); // 串口控件 this.lblPort = new Label { Text = "串口号:", TextAlign = ContentAlignment.MiddleRight, Dock = DockStyle.Fill }; this.cmbPorts = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill }; this.lblBaud = new Label { Text = "波特率:", TextAlign = ContentAlignment.MiddleRight, Dock = DockStyle.Fill }; this.cmbBaudRate = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Dock = DockStyle.Fill }; this.btnConnect = new Button { Text = "连接串口", BackColor = Color.LightGreen, Font = new Font("Microsoft YaHei UI", 10F, FontStyle.Bold), Dock = DockStyle.Fill, Margin = new Padding(5) }; // 添加控件到串口布局 serialLayout.Controls.Add(lblPort, 0, 0); serialLayout.Controls.Add(cmbPorts, 1, 0); serialLayout.Controls.Add(lblBaud, 2, 0); serialLayout.Controls.Add(cmbBaudRate, 3, 0); serialLayout.SetColumnSpan(btnConnect, 4); serialLayout.Controls.Add(btnConnect, 0, 1); gbSerial.Controls.Add(serialLayout); mainLayout.Controls.Add(gbSerial, 0, 0); // 按钮面板(包含输入挡光片宽度、光电门间距、保存历史等) this.buttonPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(10, 6, 10, 6), AutoSize = false, WrapContents = false }; // 挡光片宽度控件 this.lblPlateWidth = new Label { Text = "挡光片宽度:", TextAlign = ContentAlignment.MiddleCenter, AutoSize = true, Margin = new Padding(6, 10, 0, 0) }; this.txtPlateWidth = new TextBox { Width = 100, Text = "0.010", // 默认 10 mm (示例);用户可自行修改 Margin = new Padding(6) }; // 光电门间距控件 this.lblGateSpacing = new Label { Text = "光电门间距:", TextAlign = ContentAlignment.MiddleCenter, AutoSize = true, Margin = new Padding(6, 10, 0, 0) }; this.txtGateSpacing = new TextBox { Width = 100, Text = "0.100", // 默认 100 mm (示例) Margin = new Padding(6) }; // 保存实验按钮 this.btnSaveExperiment = new Button { Text = "保存本次实验", BackColor = Color.LightGoldenrodYellow, Size = new Size(140, 36), Margin = new Padding(6) }; // 加载历史按钮 this.btnLoadExperiment = new Button { Text = "加载历史实验", BackColor = Color.LightSteelBlue, Size = new Size(140, 36), Margin = new Padding(6) }; // 导出与清除按钮 this.btnExport = new Button { Text = "导出数据(Excel)", BackColor = Color.LightBlue, Size = new Size(180, 40), Margin = new Padding(6) }; this.btnClear = new Button { Text = "清除数据", BackColor = Color.LightSalmon, Size = new Size(180, 40), Margin = new Padding(6) }; // 把控件加入面板 buttonPanel.Controls.Add(lblPlateWidth); buttonPanel.Controls.Add(txtPlateWidth); buttonPanel.Controls.Add(lblGateSpacing); buttonPanel.Controls.Add(txtGateSpacing); buttonPanel.Controls.Add(btnSaveExperiment); buttonPanel.Controls.Add(btnLoadExperiment); buttonPanel.Controls.Add(btnExport); buttonPanel.Controls.Add(btnClear); mainLayout.Controls.Add(buttonPanel, 0, 1); // 数据表格 this.gbData = new GroupBox { Text = "实验数据", Dock = DockStyle.Fill }; this.dgvResults = new DataGridView { AllowUserToAddRows = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill, BackgroundColor = SystemColors.Window, BorderStyle = BorderStyle.None, ColumnHeadersHeight = 34, Dock = DockStyle.Fill, ReadOnly = true, RowHeadersVisible = false, ScrollBars = ScrollBars.Vertical }; // 添加列 dgvResults.Columns.Add("Time", "时间"); dgvResults.Columns.Add("T1", "T1(s)"); dgvResults.Columns.Add("T2", "T2(s)"); dgvResults.Columns.Add("V1", "V1(m/s)"); dgvResults.Columns.Add("V2", "V2(m/s)"); dgvResults.Columns.Add("Acc", "加速度(m/s²)"); gbData.Controls.Add(dgvResults); mainLayout.Controls.Add(gbData, 0, 2); // 图表 this.gbChart = new GroupBox { Text = "加速度变化曲线", Dock = DockStyle.Fill }; this.chartPlot = new Chart { Dock = DockStyle.Fill }; gbChart.Controls.Add(chartPlot); mainLayout.Controls.Add(gbChart, 0, 3); // 添加主布局到窗体 this.Controls.Add(mainLayout); this.ResumeLayout(false); } // 初始化图表 private void InitializePlot() { chartPlot.ChartAreas.Clear(); chartPlot.Series.Clear(); chartPlot.Titles.Clear(); // 创建图表区域 ChartArea chartArea = new ChartArea("MainArea") { AxisX = { Title = "实验序号" }, AxisY = { Title = "加速度 (m/s²)" } }; chartArea.AxisX.MajorGrid.LineColor = Color.LightGray; chartArea.AxisY.MajorGrid.LineColor = Color.LightGray; chartArea.AxisY.MajorGrid.LineDashStyle = ChartDashStyle.Dot; chartPlot.ChartAreas.Add(chartArea); // 创建数据系列 Series series = new Series("加速度") { ChartType = SeriesChartType.Line, Color = Color.Blue, BorderWidth = 2, MarkerStyle = MarkerStyle.Circle, MarkerSize = 8, MarkerColor = Color.DarkBlue }; chartPlot.Series.Add(series); // 添加标题 chartPlot.Titles.Add("加速度变化曲线"); chartPlot.Titles[0].Font = new Font("Microsoft YaHei UI", 12, FontStyle.Bold); chartPlot.Titles[0].ForeColor = Color.Black; // 设置初始轴范围 chartArea.AxisX.Minimum = 0; chartArea.AxisX.Maximum = 10; chartArea.AxisY.Minimum = 0; chartArea.AxisY.Maximum = 10; // 添加初始点 series.Points.AddXY(0, 0); } // 加载可用串口 private void LoadAvailablePorts() { cmbPorts.Items.Clear(); string[] ports = SerialPort.GetPortNames(); cmbPorts.Items.AddRange(ports); if (ports.Length > 0) cmbPorts.SelectedIndex = 0; // 设置波特率选项 cmbBaudRate.Items.AddRange(new object[] { 9600, 19200, 38400, 57600, 115200 }); cmbBaudRate.SelectedIndex = 4; // 默认115200 } // 连接/断开串口 private void btnConnect_Click(object sender, EventArgs e) { if (serialPort == null || !serialPort.IsOpen) { try { if (cmbPorts.SelectedItem == null) { MessageBox.Show("请选择有效的串口", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } serialPort = new SerialPort(cmbPorts.Text, (int)cmbBaudRate.SelectedItem); serialPort.DataReceived += SerialPort_DataReceived; serialPort.Open(); btnConnect.Text = "断开连接"; btnConnect.BackColor = Color.LightCoral; // 重置实验数据 experimentStartTime = DateTime.Now; dgvResults.Rows.Clear(); accelerationData.Clear(); dataPointCount = 0; InitializePlot(); } catch (Exception ex) { MessageBox.Show($"串口连接错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } else { try { serialPort.Close(); serialPort.Dispose(); serialPort = null; btnConnect.Text = "连接串口"; btnConnect.BackColor = Color.LightGreen; } catch (Exception ex) { MessageBox.Show($"断开连接错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } // 串口数据接收处理 private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { string data = serialPort.ReadLine(); this.Invoke(new Action(() => ProcessData(data))); } catch (Exception ex) { this.Invoke(new Action(() => MessageBox.Show($"数据接收错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error))); } } // 数据处理和显示 private void ProcessData(string rawData) { try { string[] pairs = rawData.Split(','); double t1 = 0, t2 = 0, v1 = 0, v2 = 0, acc = 0; foreach (string pair in pairs) { string[] keyValue = pair.Split(':'); if (keyValue.Length != 2) continue; string key = keyValue[0].Trim(); string value = keyValue[1].Trim(); switch (key) { case "T1": t1 = ParseDouble(value); break; case "T2": t2 = ParseDouble(value); break; case "V1": v1 = ParseDouble(value); break; case "V2": v2 = ParseDouble(value); break; case "A": acc = ParseDouble(value); break; } } // 计算相对时间 TimeSpan elapsed = DateTime.Now - experimentStartTime; string timeString = $"{elapsed.Minutes:00}:{elapsed.Seconds:00}.{elapsed.Milliseconds:000}"; // 添加新行到DataGridView int rowIndex = dgvResults.Rows.Add(); dgvResults.Rows[rowIndex].Cells[0].Value = timeString; dgvResults.Rows[rowIndex].Cells[1].Value = t1.ToString("0.000"); dgvResults.Rows[rowIndex].Cells[2].Value = t2.ToString("0.000"); dgvResults.Rows[rowIndex].Cells[3].Value = v1.ToString("0.000"); dgvResults.Rows[rowIndex].Cells[4].Value = v2.ToString("0.000"); dgvResults.Rows[rowIndex].Cells[5].Value = acc.ToString("0.000"); // 设置加速度单元格颜色 if (acc > 0) dgvResults.Rows[rowIndex].Cells[5].Style.BackColor = Color.FromArgb(230, 255, 230); else if (acc < 0) dgvResults.Rows[rowIndex].Cells[5].Style.BackColor = Color.FromArgb(255, 230, 230); // 自动滚动到最后一行 if (dgvResults.Rows.Count > 0) dgvResults.FirstDisplayedScrollingRowIndex = dgvResults.Rows.Count - 1; // 更新图表 UpdatePlot(acc); } catch (Exception ex) { MessageBox.Show($"数据处理错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // 解析双精度值 private double ParseDouble(string value) { return double.Parse(value.Trim(), CultureInfo.InvariantCulture); } // 更新图表数据 private void UpdatePlot(double acc) { accelerationData.Add(acc); dataPointCount++; // 限制显示点数 if (accelerationData.Count > 100) accelerationData.RemoveAt(0); // 更新图表 chartPlot.Series["加速度"].Points.Clear(); for (int i = 0; i < accelerationData.Count; i++) { chartPlot.Series["加速度"].Points.AddXY(i + 1, accelerationData[i]); } // 更新标题 chartPlot.Titles[0].Text = $"加速度变化曲线 (共 {dataPointCount} 个数据点)"; // 自动调整Y轴范围 if (accelerationData.Count > 0) { double min = accelerationData[0]; double max = accelerationData[0]; foreach (double val in accelerationData) { if (val < min) min = val; if (val > max) max = val; } double range = max - min; if (range < 0.1) range = 0.1; chartPlot.ChartAreas["MainArea"].AxisY.Minimum = min - range * 0.1; chartPlot.ChartAreas["MainArea"].AxisY.Maximum = max + range * 0.1; } // 更新X轴范围 chartPlot.ChartAreas["MainArea"].AxisX.Minimum = 1; chartPlot.ChartAreas["MainArea"].AxisX.Maximum = accelerationData.Count > 0 ? accelerationData.Count + 1 : 10; chartPlot.Invalidate(); } // 导出数据到Excel private void btnExport_Click(object sender, EventArgs e) { if (dgvResults.Rows.Count == 0) { MessageBox.Show("没有数据可导出!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using (var sfd = new System.Windows.Forms.SaveFileDialog()) { sfd.Filter = "Excel 文件|*.xlsx"; sfd.Title = "保存实验数据"; sfd.FileName = $"气垫导轨实验_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"; if (sfd.ShowDialog() == DialogResult.OK) { try { ExperimentExcelExporter.ExportToExcel( dgvResults, accelerationData, sfd.FileName, chartPlot); MessageBox.Show("数据导出成功!", "导出完成", MessageBoxButtons.OK, MessageBoxIcon.Information); // 尝试打开Excel文件 try { Process.Start(new ProcessStartInfo { FileName = sfd.FileName, UseShellExecute = true }); } catch { /* 忽略打开错误 */ } } catch (Exception ex) { MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } // 清除数据 private void btnClear_Click(object sender, EventArgs e) { if (MessageBox.Show("确定要清除所有数据吗?", "确认清除", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { dgvResults.Rows.Clear(); accelerationData.Clear(); dataPointCount = 0; experimentStartTime = DateTime.Now; InitializePlot(); } } } // Excel导出工具类 public static class ExperimentExcelExporter { public static void ExportToExcel( DataGridView dgv, List<double> accelerationData, string filePath, Chart chart = null) { using (ExcelPackage excelPackage = new ExcelPackage()) { // 创建实验信息表 CreateInfoSheet(excelPackage, dgv, accelerationData); // 创建数据工作表 CreateDataSheet(excelPackage, dgv); // 创建图表工作表 if (accelerationData.Count > 0) { CreateChartSheet(excelPackage, accelerationData); } // 添加图表图像 if (chart != null) { CreateChartImageSheet(excelPackage, chart); } // 保存文件 FileInfo excelFile = new FileInfo(filePath); excelPackage.SaveAs(excelFile); } } private static void CreateInfoSheet(ExcelPackage excelPackage, DataGridView dgv, List<double> accelerationData) { ExcelWorksheet infoSheet = excelPackage.Workbook.Worksheets.Add("实验信息"); // 标题行 infoSheet.Cells[1, 1].Value = "气垫导轨实验报告"; infoSheet.Cells[1, 1, 1, 6].Merge = true; infoSheet.Cells[1, 1].Style.Font.Bold = true; infoSheet.Cells[1, 1].Style.Font.Size = 16; infoSheet.Cells[1, 1].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; // 实验信息 infoSheet.Cells[3, 1].Value = "实验时间"; infoSheet.Cells[3, 2].Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); infoSheet.Cells[4, 1].Value = "数据点数"; infoSheet.Cells[4, 2].Value = dgv.Rows.Count; infoSheet.Cells[5, 1].Value = "平均加速度"; double avgAcc = CalculateAverage(accelerationData); infoSheet.Cells[5, 2].Value = $"{avgAcc:0.000} m/s²"; infoSheet.Cells[6, 1].Value = "最大加速度"; double maxAcc = CalculateMax(accelerationData); infoSheet.Cells[6, 2].Value = $"{maxAcc:0.000} m/s²"; // 设置列宽 infoSheet.Column(1).Width = 15; infoSheet.Column(2).Width = 25; } private static void CreateDataSheet(ExcelPackage excelPackage, DataGridView dgv) { ExcelWorksheet dataSheet = excelPackage.Workbook.Worksheets.Add("实验数据"); // 添加标题行 for (int i = 0; i < dgv.Columns.Count; i++) { dataSheet.Cells[1, i + 1].Value = dgv.Columns[i].HeaderText; dataSheet.Cells[1, i + 1].Style.Fill.PatternType = ExcelFillStyle.Solid; dataSheet.Cells[1, i + 1].Style.Fill.BackgroundColor.SetColor(Color.LightBlue); dataSheet.Cells[1, i + 1].Style.Font.Bold = true; dataSheet.Cells[1, i + 1].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; } // 添加数据行 for (int row = 0; row < dgv.Rows.Count; row++) { for (int col = 0; col < dgv.Columns.Count; col++) { var cellValue = dgv.Rows[row].Cells[col].Value; ExcelRange cell = dataSheet.Cells[row + 2, col + 1]; if (col == 0) // 时间列 { cell.Value = cellValue?.ToString(); } else if (double.TryParse(cellValue?.ToString(), out double numValue)) { cell.Value = numValue; cell.Style.Numberformat.Format = "0.000"; // 加速度列特殊样式(假设最后一列是加速度) if (dgv.Columns[col].Name == "Acc" || dgv.Columns[col].HeaderText.Contains("加速度")) { cell.Style.Font.Color.SetColor( numValue > 0 ? Color.DarkGreen : Color.DarkRed); } } else { cell.Value = cellValue?.ToString(); } } } // 自动调整列宽 dataSheet.Cells[dataSheet.Dimension.Address].AutoFitColumns(); // 添加表格边框 using (ExcelRange range = dataSheet.Cells[1, 1, dgv.Rows.Count + 1, dgv.Columns.Count]) { range.Style.Border.Top.Style = ExcelBorderStyle.Thin; range.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; range.Style.Border.Left.Style = ExcelBorderStyle.Thin; range.Style.Border.Right.Style = ExcelBorderStyle.Thin; } } private static void CreateChartSheet(ExcelPackage excelPackage, List<double> accelerationData) { ExcelWorksheet chartSheet = excelPackage.Workbook.Worksheets.Add("加速度曲线"); // 添加标题行 chartSheet.Cells[1, 1].Value = "实验序号"; chartSheet.Cells[1, 2].Value = "加速度 (m/s²)"; // 添加数据 for (int i = 0; i < accelerationData.Count; i++) { chartSheet.Cells[i + 2, 1].Value = i + 1; chartSheet.Cells[i + 2, 2].Value = accelerationData[i]; } // 创建折线图 var chart = chartSheet.Drawings.AddChart("加速度曲线", eChartType.Line) as ExcelLineChart; chart.Title.Text = "加速度变化曲线"; chart.SetPosition(1, 0, 3, 0); chart.SetSize(800, 400); // 添加数据系列 var series = chart.Series.Add( chartSheet.Cells[2, 2, accelerationData.Count + 1, 2], chartSheet.Cells[2, 1, accelerationData.Count + 1, 1]); series.Header = "加速度"; // 设置图表样式 chart.YAxis.Title.Text = "加速度 (m/s²)"; chart.XAxis.Title.Text = "实验序号"; // 添加趋势线 var trendline = series.TrendLines.Add(eTrendLine.Linear); trendline.DisplayEquation = true; } // 修复3: 正确处理Bitmap到Excel图片的转换 private static void CreateChartImageSheet(ExcelPackage excelPackage, Chart winFormsChart) { ExcelWorksheet imageSheet = excelPackage.Workbook.Worksheets.Add("图表图像"); imageSheet.Cells[1, 1].Value = "加速度变化曲线图"; imageSheet.Cells[1, 1].Style.Font.Bold = true; imageSheet.Cells[1, 1].Style.Font.Size = 14; // 使用内存流保存图像 using (Bitmap chartImage = new Bitmap(winFormsChart.Width, winFormsChart.Height)) using (MemoryStream ms = new MemoryStream()) { winFormsChart.DrawToBitmap(chartImage, new Rectangle(0, 0, winFormsChart.Width, winFormsChart.Height)); chartImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png); ms.Position = 0; // 添加图像到Excel var picture = imageSheet.Drawings.AddPicture("加速度图表", ms); picture.SetPosition(10, 5); picture.SetSize(800, 400); } } private static double CalculateAverage(List<double> data) { if (data.Count == 0) return 0; double sum = 0; foreach (double val in data) sum += val; return sum / data.Count; } private static double CalculateMax(List<double> data) { if (data.Count == 0) return 0; double max = data[0]; foreach (double val in data) if (Math.Abs(val) > Math.Abs(max)) max = val; return max; } } // 程序入口 static class Program { [STAThread] static void Main() { // 初始化 EPPlus 许可(兼容 EPPlus 8+ 与旧版本) InitEpplusLicense(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); } // 使用反射以同时兼容 EPPlus 8+ 的 ExcelPackage.License 和 旧版的 ExcelPackage.LicenseContext private static void InitEpplusLicense() { try { Type excelPackageType = typeof(ExcelPackage); // 优先尝试 EPPlus 8+:ExcelPackage.License (type OfficeOpenXml.License) PropertyInfo licenseProp = excelPackageType.GetProperty("License", BindingFlags.Static | BindingFlags.Public); if (licenseProp != null && licenseProp.CanWrite) { Type licenseType = licenseProp.PropertyType; object licenseInstance = Activator.CreateInstance(licenseType); PropertyInfo contextProp = licenseType.GetProperty("Context", BindingFlags.Instance | BindingFlags.Public); if (contextProp != null) { Type contextEnumType = contextProp.PropertyType; object nonCommercial = Enum.Parse(contextEnumType, "NonCommercial"); contextProp.SetValue(licenseInstance, nonCommercial); } licenseProp.SetValue(null, licenseInstance); return; } // 回退:旧的静态属性 ExcelPackage.LicenseContext PropertyInfo oldContextProp = excelPackageType.GetProperty("LicenseContext", BindingFlags.Static | BindingFlags.Public); if (oldContextProp != null && oldContextProp.CanWrite) { Type ctxEnumType = oldContextProp.PropertyType; object nonCommercialOld = Enum.Parse(ctxEnumType, "NonCommercial"); oldContextProp.SetValue(null, nonCommercialOld); return; } Debug.WriteLine("未找到可写的 EPPlus 许可 API(License 或 LicenseContext)。"); } catch (Exception ex) { Debug.WriteLine("EPPlus 许可初始化失败: " + ex.Message); } } } } 请你把我修复代码
11-17
内容概要:本文介绍了一个基于Matlab的综合能源系统优化调度仿真资源,重点实现了含光热电站、有机朗肯循环(ORC)和电含光热电站、有机有机朗肯循环、P2G的综合能源优化调度(Matlab代码实现)转气(P2G)技术的冷、热、电多能互补系统的优化调度模型。该模型充分考虑多种能源形式的协同转换与利用,通过Matlab代码构建系统架构、设定约束条件并求解优化目标,旨在提升综合能源系统的运行效率与经济性,同时兼顾灵活性供需不确定性下的储能优化配置问题。文中还提到了相关仿真技术支持,如YALMIP工具包的应用,适用于复杂能源系统的建模与求解。; 适合人群:具备一定Matlab编程基础和能源系统背景知识的科研人员、研究生及工程技术人员,尤其适合从事综合能源系统、可再生能源利用、电力系统优化等方向的研究者。; 使用场景及目标:①研究含光热、ORC和P2G的多能系统协调调度机制;②开展考虑不确定性的储能优化配置与经济调度仿真;③学习Matlab在能源系统优化中的建模与求解方法,复现高水平论文(如EI期刊)中的算法案例。; 阅读建议:建议读者结合文档提供的网盘资源,下载完整代码和案例文件,按照目录顺序逐步学习,重点关注模型构建逻辑、约束设置与求解器调用方式,并通过修改参数进行仿真实验,加深对综合能源系统优化调度的理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值