AS3双击DOUBLE_CLICK事件无效的解决办法


testbutton.doubleClickEnabled = true;

testbutton.mouseChildren = false;

现在我的功能都能实现,但是最近上了一台h3c V5版本的接入交换机,绑定命令和其他接入交换机不一样,绑定步骤是这样的,首先登录到这个交换机,输入display mac-address xxxx-xxxx-xxxx 回显如下<CDSL_SW201.28_LBG>dis mac-add 1c69-7a56-b2fb MAC ADDR VLAN ID STATE PORT INDEX AGING TIME(s) 1c69-7a56-b2fb 116 LEARNED GigabitEthernet1/0/7 AGING --- 1 MAC address(es) found --- 然后进入全局配置模式sys 再进入到具体的端口,如上是GigabitEthernet1/0/7 进入之后执行绑定命令 user-bind ip-address ip地址 mac-address xxxx-xxxx-xxxx(mac地址)然后再输入dis user-bind mac-address xxxx-xxxx-xxxx验证是否绑定成功,然后输入save force保存即可 因为V5的交换机不多,可以直接在代码里面写一个列表,如果在这个列表里面,就执行V5的命令 我现在的代码如下import logging import re import time import threading import tkinter as tk from tkinter import ttk, scrolledtext, messagebox from netmiko import ConnectHandler # ----------------------- 日志配置 ----------------------- logging.basicConfig( filename=&#39;ip_mac_bind.log&#39;, level=logging.INFO, format=&#39;%(asctime)s - %(levelname)s - %(message)s&#39;, encoding=&#39;utf-8&#39; ) # ----------------------- 配置参数 ----------------------- DEVICES = { &#39;oa_core_switch&#39;: { &#39;device_type&#39;: &#39;hp_comware&#39;, &#39;host&#39;: &#39;172.17.21.1&#39;, &#39;username&#39;: &#39;user&#39;, &#39;password&#39;: &#39;CD5&#39;, &#39;timeout&#39;: 30 }, &#39;aggregation_switch&#39;: { &#39;device_type&#39;: &#39;hp_comware&#39;, &#39;username&#39;: &#39;user&#39;, &#39;password&#39;: &#39;CD345&#39;, &#39;timeout&#39;: 30 }, &#39;access_switch&#39;: { &#39;device_type&#39;: &#39;hp_comware&#39;, &#39;username&#39;: &#39;python&#39;, &#39;password&#39;: &#39;C345&#39;, &#39;timeout&#39;: 30, &#39;global_delay_factor&#39;: 3 } } # 已知汇聚交换机列表 AGGREGATION_SWITCHES = ["172.17.21.2", "172.17.21.3", "172.17.21.4", "172.17.21.22"] # ----------------------- 工具函数 ----------------------- def format_mac_address(raw_mac): """格式化MAC地址为H3C格式:xxxx-xxxx-xxxx""" cleaned = re.sub(r&#39;[^0-9a-fA-F]&#39;, &#39;&#39;, raw_mac) if len(cleaned) != 12: raise ValueError("无效的MAC地址格式") return f"{cleaned[:4]}-{cleaned[4:8]}-{cleaned[8:12]}".lower() def connect_with_retry(device_info, retries=3, delay=5): """带重试机制的设备连接""" for attempt in range(retries): try: # 确保设置了合理的延迟因子 if &#39;global_delay_factor&#39; not in device_info: device_info[&#39;global_delay_factor&#39;] = 2 conn = ConnectHandler(**device_info) logging.info(f"成功连接 {device_info[&#39;host&#39;]}") # 确保禁用分页 conn.send_command_timing("screen-length disable", delay_factor=2) time.sleep(0.5) # 确保命令执行完成 return conn except Exception as e: logging.error(f"连接失败({attempt + 1}/{retries}) {device_info[&#39;host&#39;]}: {str(e)}") time.sleep(delay) raise ConnectionError(f"无法连接 {device_info[&#39;host&#39;]}") def get_device_credentials(ip): """根据IP地址确定设备类型""" if ip == DEVICES[&#39;oa_core_switch&#39;][&#39;host&#39;]: return DEVICES[&#39;oa_core_switch&#39;].copy() if ip in AGGREGATION_SWITCHES: # 汇聚交换机使用自己的凭据 creds = DEVICES[&#39;aggregation_switch&#39;].copy() creds[&#39;host&#39;] = ip return creds else: # 默认使用接入交换机凭据 creds = DEVICES[&#39;access_switch&#39;].copy() creds[&#39;host&#39;] = ip return creds def parse_switch_name(switch_name): """将交换机名称转换为IP地址""" # 支持格式: SW201.55 或 sw201.55 pattern = r&#39;^[sS][wW](\d{1,3})\.(\d{1,3})$&#39; match = re.match(pattern, switch_name) if not match: raise ValueError("交换机名称格式错误,请使用 SW交换机编号 的格式") building = match.group(1) switch_num = match.group(2) # 验证数据在合理范围内 if not 0 <= int(building) <= 255: raise ValueError(f"数据超出范围: {building}") # 验证交换机编号在合理范围内 if not 0 <= int(switch_num) <= 255: raise ValueError(f"交换机编号超出范围: {switch_num}") return f"172.17.{building}.{switch_num}" # ----------------------- 核心功能 ----------------------- def find_port_with_mac(conn, mac): """查找MAC地址所在的端口(支持单链路和聚合端口)""" try: output = conn.send_command_timing(f"display mac-address {mac}", delay_factor=4) logging.info(f"MAC地址查询结果 ({conn.host}):\n{output}") # 查找端口(可能是聚合端口或普通端口) found_port = None # 正则表达式匹配端口 port_pattern = r&#39;(BAGG\d+|GE\S+|XGE\S+|Ten-GigabitEthernet\S+)&#39; for line in output.splitlines(): # 查找Learned状态的端口 if &#39;Learned&#39; in line: # 优先查找聚合端口 if &#39;BAGG&#39; in line: match = re.search(r&#39;BAGG\d+&#39;, line) if match: found_port = match.group() logging.info(f"在{conn.host}上找到聚合端口: {found_port}") return found_port, "AGG" # 查找物理端口 elif &#39;GE&#39; in line or &#39;XGE&#39; in line or &#39;Ten-GigabitEthernet&#39; in line: match = re.search(port_pattern, line) if match: found_port = match.group() logging.info(f"在{conn.host}上找到物理端口: {found_port}") return found_port, "PHY" # 如果没找到明确的端口,尝试普通查询 for line in output.splitlines(): if &#39;GE&#39; in line or &#39;XGE&#39; in line or &#39;BAGG&#39; in line or &#39;Ten-GigabitEthernet&#39; in line: match = re.search(port_pattern, line) if match: found_port = match.group() if &#39;BAGG&#39; in found_port: logging.info(f"在{conn.host}上找到聚合端口: {found_port}") return found_port, "AGG" else: logging.info(f"在{conn.host}上找到物理端口: {found_port}") return found_port, "PHY" logging.warning(f"在{conn.host}上未找到明确的端口") return None, None except Exception as e: logging.error(f"MAC地址查询失败 ({conn.host}): {str(e)}") return None, None def parse_lldp_neighbor(output): """解析LLDP输出获取管理IP""" for line in output.splitlines(): if &#39;Management address&#39; in line and &#39;:&#39; in line: ip_match = re.search(r&#39;\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b&#39;, line) if ip_match: return ip_match.group() return None def get_lldp_neighbor(conn, port): """获取LLDP邻居信息""" try: # 确保端口格式正确,移除多余空格和括号 port = re.sub(r&#39;[()]&#39;, &#39;&#39;, port).strip() # H3C端口格式转换 if port.startswith(&#39;XGE&#39;): converted_port = port.replace("XGE", "Ten-GigabitEthernet") elif port.startswith(&#39;GE&#39;): converted_port = port.replace("GE", "GigabitEthernet") else: converted_port = port logging.info(f"在{conn.host}上查询接口 {converted_port} 的LLDP邻居信息") output = conn.send_command_timing( f"display lldp neighbor-information interface {converted_port} verbose", read_timeout=90, delay_factor=4 ) return parse_lldp_neighbor(output) except Exception as e: logging.error(f"在{conn.host}上LLDP查询失败: {str(e)}") return None def get_agg_members(conn, agg_port): """获取聚合组的成员端口(改进的解析逻辑)""" try: # 将聚合端口转换为标准格式(如BAGG12 -> Bridge-Aggregation12) if agg_port.startswith(&#39;BAGG&#39;): bridge_agg = "Bridge-Aggregation" + agg_port[4:] else: bridge_agg = agg_port logging.info(f"查询聚合组: {bridge_agg}") # 使用正确的命令格式 output = conn.send_command_timing( f"display link-aggregation verbose {bridge_agg}", delay_factor=4 ) logging.info(f"聚合组查询结果:\n{output}") # 解析成员端口 - 改进的解析逻辑 members = [] in_local_section = False for line in output.splitlines(): # 查找Local部分标题 if "Local:" in line: in_local_section = True continue if in_local_section: # 检查是否进入Remote部分 if "Remote:" in line: break # 忽略空行 if not line.strip(): continue # 提取物理端口(行中第一个元素) parts = line.split() if parts and any(port_type in parts[0] for port_type in [&#39;GE&#39;, &#39;XGE&#39;, &#39;Ten&#39;]): port_name = parts[0] # 移除可能的参考端口标识 (R) if &#39;(&#39; in port_name: port_name = port_name.split(&#39;(&#39;)[0] members.append(port_name) logging.info(f"找到成员端口: {port_name}") if not members: # 尝试备用解析方法 logging.warning("使用备用方法解析成员端口") for line in output.splitlines(): if any(port_type in line for port_type in [&#39;GE&#39;, &#39;XGE&#39;, &#39;Ten&#39;]): match = re.search(r&#39;(GE\S+|XGE\S+|Ten-GigabitEthernet\S+)&#39;, line) if match: port_name = match.group() if &#39;(&#39; in port_name: port_name = port_name.split(&#39;(&#39;)[0] if port_name not in members: members.append(port_name) logging.info(f"找到成员端口: {port_name}") if not members: logging.warning(f"未能在聚合组 {bridge_agg} 中找到成员端口") else: logging.info(f"找到 {len(members)} 个成员端口") return members except Exception as e: logging.error(f"聚合组查询失败: {str(e)}") return [] def get_next_hop_from_port(conn, port, port_type): """根据端口类型获取下一跳设备""" if port_type == "AGG": logging.info(f"处理聚合端口: {port}") # 获取聚合组中的所有物理端口 physical_ports = get_agg_members(conn, port) if not physical_ports: logging.warning(f"在{conn.host}上未找到聚合端口 {port} 的成员端口") return None # 尝试每个物理端口查找LLDP邻居 for physical_port in physical_ports: logging.info(f"在物理端口 {physical_port} 上查询LLDP邻居") neighbor_ip = get_lldp_neighbor(conn, physical_port) if neighbor_ip: logging.info(f"在端口 {physical_port} 上找到邻居: {neighbor_ip}") return neighbor_ip return None else: logging.info(f"处理物理端口: {port}") return get_lldp_neighbor(conn, port) def trace_next_hop(switch_ip, mac): """追踪下一跳设备(改进的聚合端口处理)""" conn = None try: # 获取设备凭据 device_info = get_device_credentials(switch_ip) conn = connect_with_retry(device_info) # 查找MAC地址所在的端口及其类型 port, port_type = find_port_with_mac(conn, mac) if not port: logging.warning(f"在{switch_ip}上未找到MAC地址 {mac} 对应的端口") return None # 查找邻居设备IP neighbor_ip = get_next_hop_from_port(conn, port, port_type) if neighbor_ip: logging.info(f"在{switch_ip}上端口 {port} 找到邻居设备: {neighbor_ip}") return neighbor_ip else: logging.warning(f"在{switch_ip}上端口 {port} 未找到LLDP邻居") return None except Exception as e: logging.error(f"在{switch_ip}上追踪失败: {str(e)}") return None finally: if conn: conn.disconnect() def trace_final_device(mac): """追踪到最终设备""" formatted_mac = format_mac_address(mac) logging.info(f"开始追踪 MAC: {formatted_mac}") # 第一跳:核心交换机 core_ip = DEVICES[&#39;oa_core_switch&#39;][&#39;host&#39;] first_hop = trace_next_hop(core_ip, formatted_mac) if not first_hop: logging.warning("无法从核心交换机找到第一跳设备") return None # 检查第一跳设备是否是汇聚交换机 if first_hop in AGGREGATION_SWITCHES: logging.info(f"{first_hop} 是已知汇聚交换机,继续追踪") # 第二跳:从汇聚交换机追踪 final_device = trace_next_hop(first_hop, formatted_mac) if not final_device: logging.info(f"在汇聚交换机{first_hop}上未找到下层设备,使用该交换机作为最终设备") return first_hop return final_device else: logging.info(f"{first_hop} 不是已知汇聚交换机,作为最终设备") return first_hop def configure_device(device_ip, ip, mac): """在最终设备上绑定IP-MAC""" conn = None try: # 获取设备凭据 device_info = get_device_credentials(device_ip) # 为接入交换机设置更高的延迟因子 device_info[&#39;global_delay_factor&#39;] = 3 conn = connect_with_retry(device_info) # 进入系统视图 - 使用send_command_timing避免期望字符串问题 conn.send_command_timing("system-view", delay_factor=3) time.sleep(1) # 确保进入系统视图 # 绑定IP-MAC cmd = f"ip source binding ip-address {ip} mac-address {mac}" output = conn.send_command_timing(cmd, delay_factor=3) logging.info(f"在{device_ip}上执行命令: {cmd}\n输出: {output}") # 保存配置 - 使用更稳健的方式处理保存确认 save_output = conn.send_command_timing("save force", delay_factor=3) if "Are you sure" in save_output or "(Y/N)" in save_output: save_output += conn.send_command_timing("y", delay_factor=3) time.sleep(2) # 给保存操作足够的时间 logging.info(f"在{device_ip}上保存配置结果: {save_output}") # 检查是否成功绑定 verify_cmd = f"display ip source binding ip-address {ip}" verify_output = conn.send_command_timing(verify_cmd, delay_factor=3) logging.info(f"验证绑定: {verify_output}") # 确认MAC地址是否在输出中 if mac.lower() in verify_output.lower(): logging.info(f"在{device_ip}上成功绑定验证") return True, device_ip else: logging.warning(f"在{device_ip}上未找到绑定记录") return False, device_ip except Exception as e: logging.error(f"在{device_ip}上配置失败: {str(e)}") return False, device_ip finally: if conn: try: conn.disconnect() except: pass def bind_ip_mac(ip, mac): """主绑定流程(自动发现)""" try: formatted_mac = format_mac_address(mac) logging.info(f"开始处理 自动发现: IP: {ip}, MAC: {formatted_mac}") # 查找最终设备 device_ip = trace_final_device(formatted_mac) if not device_ip: return f"❌ 错误: 无法找到MAC {formatted_mac} 对应的网络设备" # 在最终设备上配置 success, target_ip = configure_device(device_ip, ip, formatted_mac) return f"✅ 成功: 在 {target_ip} 上绑定 {ip} - {formatted_mac}" if success else \ f"❌ 错误: 无法在 {target_ip} 上绑定 {ip} - {formatted_mac}" except Exception as e: error_msg = f"⚠️ 异常: {str(e)}" logging.error(error_msg) return error_msg def manual_bind(ip, mac, switch_name): """手动绑定流程""" try: formatted_mac = format_mac_address(mac) switch_ip = parse_switch_name(switch_name) logging.info(f"开始处理 手动绑定: IP: {ip}, MAC: {formatted_mac}, 交换机: {switch_name} -> {switch_ip}") # 在指定交换机上配置 success, target_ip = configure_device(switch_ip, ip, formatted_mac) return f"✅ 成功: 在 {target_ip} 上手动绑定 {ip} - {formatted_mac}" if success else \ f"❌ 错误: 无法在 {target_ip} 上手动绑定 {ip} - {formatted_mac}" except ValueError as ve: error_msg = f"⚠️ 输入错误: {str(ve)}" logging.error(error_msg) return error_msg except Exception as e: error_msg = f"⚠️ 异常: {str(e)}" logging.error(error_msg) return error_msg # ----------------------- GUI界面 ----------------------- class IPMacBindApp: def __init__(self, root): self.root = root self.root.title("IP-MAC绑定工具") self.root.geometry("900x750") # 调整尺寸以适应新输入区域 self.root.resizable(True, True) # 设置现代主题 style = ttk.Style() style.theme_use(&#39;clam&#39;) # 配置颜色 style.configure("TFrame", background="#f0f0f0") style.configure("TLabel", background="#f0f0f0", foreground="#333333") style.configure("TButton", background="#4a86e8", foreground="white", font=("Arial", 10)) style.map("TButton", background=[(&#39;active&#39;, &#39;#3a76d8&#39;)]) style.configure("Accent.TButton", background="#28a745", foreground="white", font=("Arial", 10, "bold")) style.map("Accent.TButton", background=[(&#39;active&#39;, &#39;#218838&#39;)]) style.configure("Treeview", background="white", fieldbackground="white", foreground="#333333") style.configure("Treeview.Heading", background="#e9ecef", foreground="#333333", font=("Arial", 9, "bold")) style.configure("TLabelframe", background="#f0f0f0", foreground="#333333") style.configure("TLabelframe.Label", background="#f0f0f0", foreground="#333333") style.configure("TScrollbar", background="#e9ecef") # 配置操作按钮样式 style.configure("Delete.TButton", background="#e74c3c", foreground="white", font=("Arial", 9, "bold")) style.map("Delete.TButton", background=[(&#39;active&#39;, &#39;#c0392b&#39;)]) # 创建主框架 main_frame = ttk.Frame(root) main_frame.pack(fill="both", expand=True, padx=15, pady=15) # 标题区域 title_frame = ttk.Frame(main_frame) title_frame.pack(fill="x", pady=(0, 10)) title_label = ttk.Label( title_frame, text="IP-MAC绑定工具", font=("Arial", 16, "bold"), foreground="#2c3e50" ) title_label.pack(pady=5) # ========== 新增手动绑定区域 ========== manual_frame = ttk.LabelFrame(main_frame, text="手动交换机绑定") manual_frame.pack(fill="x", pady=10, padx=5) # 使用网格布局 manual_grid = ttk.Frame(manual_frame) manual_grid.pack(fill="x", padx=10, pady=10) # 交换机名称标签和输入框 ttk.Label(manual_grid, text="交换机名称:").grid(row=0, column=0, padx=5, pady=5, sticky="e") self.switch_entry = ttk.Entry(manual_grid, width=20) self.switch_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w") self.switch_entry.insert(0, "SW201.55") # 提示文本 tip_label = ttk.Label( manual_grid, text="格式: SW交换机号 (示例: SW201.55 -> 172.17.201.55)", foreground="#666666", font=("Arial", 8) ) tip_label.grid(row=0, column=2, padx=10, pady=5, sticky="w", columnspan=3) # IP地址标签和输入框 ttk.Label(manual_grid, text="IP地址:").grid(row=1, column=0, padx=5, pady=5, sticky="e") self.manual_ip_entry = ttk.Entry(manual_grid, width=20) self.manual_ip_entry.grid(row=1, column=1, padx=5, pady=5, sticky="w") # MAC地址标签和输入框 ttk.Label(manual_grid, text="MAC地址:").grid(row=1, column=2, padx=5, pady=5, sticky="e") self.manual_mac_entry = ttk.Entry(manual_grid, width=20) self.manual_mac_entry.grid(row=1, column=3, padx=5, pady=5, sticky="w") # 添加手动绑定按钮 manual_add_btn = ttk.Button( manual_grid, text="添加手动绑定任务", command=self.add_manual_to_queue, width=18, style="Accent.TButton" ) manual_add_btn.grid(row=1, column=4, padx=10, pady=5) # 添加分隔线 separator = ttk.Separator(main_frame, orient="horizontal") separator.pack(fill="x", pady=10) # ========== 自动绑定区域 ========== auto_frame = ttk.LabelFrame(main_frame, text="自动发现绑定 (通过LLDP)") auto_frame.pack(fill="x", pady=10, padx=5) # 使用网格布局 auto_grid = ttk.Frame(auto_frame) auto_grid.pack(fill="x", padx=10, pady=10) ttk.Label(auto_grid, text="IP地址:").grid(row=0, column=0, padx=5, pady=5, sticky="e") self.auto_ip_entry = ttk.Entry(auto_grid, width=20) self.auto_ip_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w") ttk.Label(auto_grid, text="MAC地址:").grid(row=0, column=2, padx=5, pady=5, sticky="e") self.auto_mac_entry = ttk.Entry(auto_grid, width=20) self.auto_mac_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w") auto_add_btn = ttk.Button( auto_grid, text="添加到队列 (自动发现)", command=self.add_auto_to_queue, width=18, style="TButton" ) auto_add_btn.grid(row=0, column=4, padx=10, pady=5) # 分隔线下方的队列区域 separator2 = ttk.Separator(main_frame, orient="horizontal") separator2.pack(fill="x", pady=5) # 队列区域 queue_frame = ttk.LabelFrame(main_frame, text="绑定任务队列") queue_frame.pack(fill="both", expand=True, pady=5) # 创建队列树视图(添加操作列) columns = ("type", "ip", "mac", "switch", "status", "actions") self.queue_tree = ttk.Treeview( queue_frame, columns=columns, show="headings", height=8, selectmode="browse" ) self.queue_tree.heading("type", text="类型") self.queue_tree.heading("ip", text="IP地址") self.queue_tree.heading("mac", text="MAC地址") self.queue_tree.heading("switch", text="目标交换机") self.queue_tree.heading("status", text="状态") self.queue_tree.heading("actions", text="操作") self.queue_tree.column("type", width=80, anchor="center") self.queue_tree.column("ip", width=120, anchor="center") self.queue_tree.column("mac", width=150, anchor="center") self.queue_tree.column("switch", width=150, anchor="center") self.queue_tree.column("status", width=100, anchor="center") self.queue_tree.column("actions", width=80, anchor="center") # 配置状态颜色 self.queue_tree.tag_configure(&#39;waiting&#39;, foreground=&#39;#6c757d&#39;) self.queue_tree.tag_configure(&#39;processing&#39;, foreground=&#39;#0d6efd&#39;) self.queue_tree.tag_configure(&#39;manual&#39;, foreground=&#39;#e67e22&#39;) self.queue_tree.tag_configure(&#39;auto&#39;, foreground=&#39;#3498db&#39;) self.queue_tree.tag_configure(&#39;success&#39;, foreground=&#39;#198754&#39;) self.queue_tree.tag_configure(&#39;failed&#39;, foreground=&#39;#dc3545&#39;) # 添加滚动条 tree_scroll = ttk.Scrollbar( queue_frame, orient="vertical", command=self.queue_tree.yview ) self.queue_tree.configure(yscrollcommand=tree_scroll.set) # 布局 self.queue_tree.pack(side="left", fill="both", expand=True, padx=5, pady=5) tree_scroll.pack(side="right", fill="y", padx=5, pady=5) # 操作按钮区域 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill="x", pady=10) # 添加开始绑定按钮 self.start_btn = ttk.Button( btn_frame, text="开始绑定", command=self.start_binding, width=12, style="Accent.TButton" ) self.start_btn.pack(side="left", padx=5) # 添加重试失败按钮 self.retry_btn = ttk.Button( btn_frame, text="重试失败", command=self.retry_failed, width=12, style="TButton" ) self.retry_btn.pack(side="left", padx=5) # 其他操作按钮 clear_queue_btn = ttk.Button( btn_frame, text="清空队列", command=self.clear_queue, width=12, style="TButton" ) clear_queue_btn.pack(side="left", padx=5) export_btn = ttk.Button( btn_frame, text="导出日志", command=self.export_log, width=12, style="TButton" ) export_btn.pack(side="right", padx=5) # 日志区域 log_frame = ttk.LabelFrame(main_frame, text="操作日志") log_frame.pack(fill="both", expand=True, pady=(0, 5)) self.log_area = scrolledtext.ScrolledText( log_frame, height=15, bg="white", fg="#333333" ) self.log_area.pack(fill="both", expand=True, padx=5, pady=5) self.log_area.configure(state="disabled") # 状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label( root, textvariable=self.status_var, relief="sunken", anchor="w", background="#e9ecef", foreground="#6c757d", font=("Arial", 9) ) status_bar.pack(side="bottom", fill="x", padx=5, pady=2) # 状态变量 self.running = False self.queue = [] # 存储任务信息:((type, ip, mac, switch_name), status) self.task_ids = {} # 跟踪任务ID,防止重复添加未完成的任务 self.item_ids = {} # 跟踪树项ID,防止重复添加 # 绑定双击事件 self.queue_tree.bind("<Double-1>", self.on_tree_double_click) # 绑定右键菜单 self.context_menu = tk.Menu(root, tearoff=0) self.context_menu.add_command(label="删除任务", command=self.delete_selected_task) self.queue_tree.bind("<Button-3>", self.show_context_menu) self.log_message("✅ 应用程序已启动") self.log_message("手动绑定区域: 输入交换机名(如SW201.55)、IP和MAC,点&#39;添加手动绑定任务&#39;") self.log_message("自动绑定区域: 输入IP和MAC,点&#39;添加到队列(自动发现)&#39;") def log_message(self, message): """添加消息到日志区域""" self.log_area.configure(state="normal") self.log_area.insert(tk.END, f"{time.strftime(&#39;%H:%M:%S&#39;)} - {message}\n") self.log_area.see(tk.END) self.log_area.configure(state="disabled") self.status_var.set(message[:60] + "..." if len(message) > 60 else message) def add_auto_to_queue(self): """添加自动绑定任务到队列""" ip = self.auto_ip_entry.get().strip() mac = self.auto_mac_entry.get().strip() if not ip or not mac: messagebox.showwarning("输入错误", "请输入IP和MAC地址") return try: # 验证MAC格式 formatted_mac = format_mac_address(mac) # 检查IP格式 if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip): raise ValueError("无效的IP地址格式") # 创建任务标识符 task_type = "auto" task_id = f"{task_type}|{ip}|{mac}" # 检查任务是否已存在且未完成 if task_id in self.task_ids: existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4] if existing_status in ["等待中", "处理中..."]: self.log_message(f"⚠️ 任务已存在: {ip} ↔ {formatted_mac} (自动发现)") return # 添加到队列 self.queue.append((task_type, ip, mac, "")) # 添加到队列树 item_id = self.queue_tree.insert( "", "end", values=("自动发现", ip, formatted_mac, "待确定", "等待中", "删除"), tags=(&#39;auto&#39;, &#39;waiting&#39;) ) self.task_ids[task_id] = item_id self.item_ids[item_id] = task_id # 添加反向映射 # 清空输入框 self.auto_ip_entry.delete(0, tk.END) self.auto_mac_entry.delete(0, tk.END) self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (自动发现)") self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理") except Exception as e: self.log_message(f"❌ 错误: {str(e)}") def add_manual_to_queue(self): """添加手动绑定任务到队列""" switch_name = self.switch_entry.get().strip() ip = self.manual_ip_entry.get().strip() mac = self.manual_mac_entry.get().strip() if not switch_name or not ip or not mac: messagebox.showwarning("输入错误", "请输入交换机名称、IP和MAC地址") return try: # 验证MAC格式 formatted_mac = format_mac_address(mac) # 检查IP格式 if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip): raise ValueError("无效的IP地址格式") # 解析交换机名称 switch_ip = parse_switch_name(switch_name) # 创建任务标识符 task_type = "manual" task_id = f"{task_type}|{switch_ip}|{ip}|{formatted_mac}" # 检查任务是否已存在且未完成 if task_id in self.task_ids: existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4] if existing_status in ["等待中", "处理中..."]: self.log_message(f"⚠️ 任务已存在: {switch_name} ↔ {ip} ↔ {formatted_mac}") return # 添加到队列 self.queue.append((task_type, ip, mac, switch_name)) # 添加到队列树 item_id = self.queue_tree.insert( "", "end", values=("手动指定", ip, formatted_mac, f"{switch_name} ({switch_ip})", "等待中", "删除"), tags=(&#39;manual&#39;, &#39;waiting&#39;) ) self.task_ids[task_id] = item_id self.item_ids[item_id] = task_id # 添加反向映射 # 清空输入框(只清空IP和MAC,保留交换机名称) self.manual_ip_entry.delete(0, tk.END) self.manual_mac_entry.delete(0, tk.END) self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (交换机: {switch_name} -> {switch_ip})") self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理") except Exception as e: self.log_message(f"❌ 错误: {str(e)}") def on_tree_double_click(self, event): """处理树状视图的双击事件""" region = self.queue_tree.identify("region", event.x, event.y) if region != "cell": return column = self.queue_tree.identify_column(event.x) item_id = self.queue_tree.identify_row(event.y) if not item_id: return # 如果双击的是操作列(第6列) if column == "#6": self.confirm_delete_task(item_id) def confirm_delete_task(self, item_id): """确认删除任务""" values = self.queue_tree.item(item_id, "values") if not values: return if values[4] == "处理中...": # 第5列是状态 messagebox.showwarning("无法删除", "任务正在处理中,无法删除") return task_type = values[0] # 第1列是类型 ip = values[1] # 第2列是IP mac = values[2] # 第3列是MAC if messagebox.askyesno("确认删除", f"确定要删除 {task_type} 绑定任务吗?\nIP: {ip}\nMAC: {mac}"): self.delete_task(item_id) def delete_task(self, item_id): """从队列中删除任务""" if item_id not in self.item_ids: self.log_message(f"❌ 错误: 树项 {item_id} 不存在") return # 获取任务ID task_id = self.item_ids[item_id] # 从树视图中删除 self.queue_tree.delete(item_id) # 从task_ids字典中移除 if task_id in self.task_ids: del self.task_ids[task_id] # 从item_ids字典中移除 if item_id in self.item_ids: del self.item_ids[item_id] # 从队列中移除任务 task_parts = task_id.split("|") if len(task_parts) > 1: task_type = task_parts[0] ip = task_parts[1] mac = task_parts[2] if len(task_parts) >= 3 else "" switch = task_parts[3] if len(task_parts) >= 4 else "" # 从队列中删除相应的任务 for task in self.queue[:]: if ( task[0] == task_type and task[1] == ip and task[2] == mac and (len(task) < 4 or task[3] == switch) ): self.queue.remove(task) break self.log_message("♻️ 任务已删除") self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理") def show_context_menu(self, event): """显示右键菜单""" item_id = self.queue_tree.identify_row(event.y) if not item_id: return values = self.queue_tree.item(item_id, "values") if not values or len(values) < 6: return self.queue_tree.selection_set(item_id) # 选中该行 # 创建带删除选项的菜单 self.context_menu.delete(0, tk.END) if values[4] == "处理中...": # 状态在第5列 self.context_menu.add_command( label="任务处理中(不可删除)" ) else: self.context_menu.add_command( label="删除任务", command=lambda id=item_id: self.confirm_delete_task(id) ) self.context_menu.tk_popup(event.x_root, event.y_root) def delete_selected_task(self): """删除当前选中的任务""" selected = self.queue_tree.selection() if selected: self.confirm_delete_task(selected[0]) # 在 add_manual_to_queue 方法中修改任务ID生成方式 def add_manual_to_queue(self): """添加手动绑定任务到队列""" switch_name = self.switch_entry.get().strip() ip = self.manual_ip_entry.get().strip() mac = self.manual_mac_entry.get().strip() if not switch_name or not ip or not mac: messagebox.showwarning("输入错误", "请输入交换机名称、IP和MAC地址") return try: # 验证MAC格式 formatted_mac = format_mac_address(mac) # 检查IP格式 if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip): raise ValueError("无效的IP地址格式") # 解析交换机名称 switch_ip = parse_switch_name(switch_name) # 创建任务标识符 - 使用交换机IP而不是名称 task_type = "manual" task_id = f"{task_type}|{switch_ip}|{ip}|{formatted_mac}" # 关键修改 # 检查任务是否已存在且未完成 if task_id in self.task_ids: existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4] if existing_status in ["等待中", "处理中..."]: self.log_message(f"⚠️ 任务已存在: {switch_name} ↔ {ip} ↔ {formatted_mac}") return # 添加到队列 self.queue.append((task_type, ip, mac, switch_name)) # 添加到队列树 item_id = self.queue_tree.insert( "", "end", values=("手动指定", ip, formatted_mac, f"{switch_name} ({switch_ip})", "等待中", "删除"), tags=(&#39;manual&#39;, &#39;waiting&#39;) ) self.task_ids[task_id] = item_id self.item_ids[item_id] = task_id # 添加反向映射 # 清空输入框(只清空IP和MAC,保留交换机名称) self.manual_ip_entry.delete(0, tk.END) self.manual_mac_entry.delete(0, tk.END) self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (交换机: {switch_name} -> {switch_ip})") self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理") except Exception as e: self.log_message(f"❌ 错误: {str(e)}") # 在 start_binding 方法中修改任务ID生成方式 # 修改 add_auto_to_queue 方法 def add_auto_to_queue(self): """添加自动绑定任务到队列""" ip = self.auto_ip_entry.get().strip() mac = self.auto_mac_entry.get().strip() if not ip or not mac: messagebox.showwarning("输入错误", "请输入IP和MAC地址") return try: # 验证MAC格式 formatted_mac = format_mac_address(mac) # 检查IP格式 if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip): raise ValueError("无效的IP地址格式") # 创建任务标识符 task_type = "auto" task_id = f"{task_type}|{ip}|{formatted_mac}" # 使用格式化后的MAC # 检查任务是否已存在且未完成 if task_id in self.task_ids: existing_status = self.queue_tree.item(self.task_ids[task_id], "values")[4] if existing_status in ["等待中", "处理中..."]: self.log_message(f"⚠️ 任务已存在: {ip} ↔ {formatted_mac} (自动发现)") return # 添加到队列 - 只存储3个元素 self.queue.append((task_type, ip, mac)) # 添加到队列树 item_id = self.queue_tree.insert( "", "end", values=("自动发现", ip, formatted_mac, "待确定", "等待中", "删除"), tags=(&#39;auto&#39;, &#39;waiting&#39;) ) self.task_ids[task_id] = item_id self.item_ids[item_id] = task_id # 添加反向映射 # 清空输入框 self.auto_ip_entry.delete(0, tk.END) self.auto_mac_entry.delete(0, tk.END) self.log_message(f"✅ 已添加绑定任务: {ip} ↔ {formatted_mac} (自动发现)") self.status_var.set(f"队列: {len(self.queue)} 个任务等待处理") except Exception as e: self.log_message(f"❌ 错误: {str(e)}") # 修改 start_binding 方法中的任务处理部分 def start_binding(self): """开始绑定流程(多线程)""" if not self.queue: messagebox.showinfo("操作提示", "绑定队列为空") return if self.running: self.log_message("⚠️ 绑定任务已在执行中") return self.running = True self.start_btn.configure(state="disabled") self.retry_btn.configure(state="disabled") self.log_message(" 开始绑定任务处理...") # 创建线程执行绑定 def worker(): try: # 创建队列副本避免修改时冲突 queue_copy = self.queue.copy() for task_info in queue_copy: # 使用[:]创建副本避免修改冲突 if not self.running: self.log_message("❌ 绑定任务被中断") break # 解包任务信息 task_type = task_info[0] ip = task_info[1] mac = task_info[2] # 格式化MAC地址 formatted_mac = format_mac_address(mac) # 创建任务标识符 if task_type == "manual": switch_name = task_info[3] switch_ip = parse_switch_name(switch_name) task_id = f"{task_type}|{switch_ip}|{ip}|{formatted_mac}" else: # auto task_id = f"{task_type}|{ip}|{formatted_mac}" item_id = self.task_ids.get(task_id) # 检查任务是否已被删除 if not item_id or not self.queue_tree.exists(item_id): self.log_message(f"⏩ 跳过已删除任务: {ip} ↔ {formatted_mac}") continue # 更新任务状态 self.queue_tree.item( item_id, values=(task_type.capitalize(), ip, formatted_mac, self.queue_tree.item(item_id, "values")[3], "处理中...", "删除"), tags=(&#39;processing&#39;,) ) self.log_message(f"▶ 正在处理: {task_type}任务 - {ip} ↔ {formatted_mac}") # 执行绑定 start_time = time.time() if task_type == "manual": result = manual_bind(ip, mac, switch_name) switch_ip = parse_switch_name(switch_name) # 更新交换机显示 self.queue_tree.item( item_id, values=("手动指定", ip, formatted_mac, f"{switch_name} ({switch_ip})", "处理中...", "删除"), tags=(&#39;processing&#39;,) ) else: # auto # 调用自动绑定函数 result = bind_ip_mac(ip, mac) elapsed = time.time() - start_time self.log_message(f"⏱ 处理耗时: {elapsed:.2f}秒") self.log_message(result) # 解析结果并更新状态 if "成功" in result: status = "✅ 已完成" tag = &#39;success&#39; # 自动任务需要更新交换机信息 if task_type == "auto": # 尝试从结果中提取设备IP (例如 "✅ 成功: 在 172.17.201.55 上绑定...") match = re.search(r&#39;在\s*(\d+\.\d+\.\d+\.\d+)\s*上&#39;, result) if match: device_ip = match.group(1) self.queue_tree.item( item_id, values=("自动发现", ip, formatted_mac, device_ip, status, "删除"), tags=(tag,) ) else: self.queue_tree.item( item_id, values=("自动发现", ip, formatted_mac, "已发现", status, "删除"), tags=(tag,) ) else: # manual self.queue_tree.item( item_id, values=("手动指定", ip, formatted_mac, self.queue_tree.item(item_id, "values")[3], status, "删除"), tags=(tag,) ) else: status = "❌ 失败" tag = &#39;failed&#39; self.queue_tree.item( item_id, values=(task_type.capitalize(), ip, formatted_mac, self.queue_tree.item(item_id, "values")[3], status, "删除"), tags=(tag,) ) # 处理完成后从主队列中移除 if task_info in self.queue: self.queue.remove(task_info) # 短暂暂停避免设备过载 time.sleep(1) self.log_message("🏁 所有绑定任务处理完成") except Exception as e: self.log_message(f"⚠️ 绑定过程中断: {str(e)}") finally: self.start_btn.configure(state="normal") self.retry_btn.configure(state="normal") self.running = False self.status_var.set("就绪 | 绑定任务完成") threading.Thread(target=worker, daemon=True).start() def retry_failed(self): """重试所有失败的任务""" # 收集所有失败的任务 failed_tasks = [] for item_id in self.queue_tree.get_children(): values = self.queue_tree.item(item_id, "values") if values and values[4] == "❌ 失败": # 第5列是状态 ip, mac = values[1], values[2] task_type = values[0] if task_type == "手动指定": switch_name_match = re.search(r&#39;(\S+)\s*\(&#39;, values[3]) # 从"SW201.55 (172.17.201.55)"提取"SW201.55" if switch_name_match: switch_name = switch_name_match.group(1) failed_tasks.append((item_id, "manual", ip, mac, switch_name)) else: failed_tasks.append((item_id, "auto", ip, mac, "")) if not failed_tasks: messagebox.showinfo("操作提示", "没有失败的任务") return self.log_message(f"🔄 开始重试 {len(failed_tasks)} 个失败任务") # 直接开始绑定,不修改队列 self.running = True self.start_btn.configure(state="disabled") self.retry_btn.configure(state="disabled") # 创建线程执行重试 def worker(): try: for task_info in failed_tasks: item_id, task_type, ip, mac, switch_name = task_info if not self.running: self.log_message("❌ 重试任务被中断") break # 检查树项是否存在 if not self.queue_tree.exists(item_id): self.log_message(f"⏩ 跳过不存在的任务: {ip} ↔ {mac}") continue # 更新任务状态 self.queue_tree.item( item_id, values=(self.queue_tree.item(item_id, "values")[0], ip, format_mac_address(mac), self.queue_tree.item(item_id, "values")[3], "处理中...", "删除"), tags=(&#39;processing&#39;,) ) self.log_message(f"▶ 正在重试: {task_type}任务 - {ip} ↔ {format_mac_address(mac)}") # 执行绑定 start_time = time.time() if task_type == "manual": result = manual_bind(ip, mac, switch_name) else: # auto result = bind_ip_mac(ip, mac) elapsed = time.time() - start_time self.log_message(f"⏱ 处理耗时: {elapsed:.2f}秒") self.log_message(result) # 更新任务状态 if "成功" in result: status = "✅ 已完成" tag = &#39;success&#39; else: status = "❌ 失败" tag = &#39;failed&#39; self.queue_tree.item( item_id, values=(task_type, ip, format_mac_address(mac), self.queue_tree.item(item_id, "values")[3], status, "删除"), tags=(tag,) ) # 短暂暂停避免设备过载 time.sleep(1) self.log_message("🏁 所有重试任务处理完成") except Exception as e: self.log_message(f"⚠️ 重试过程中断: {str(e)}") finally: self.start_btn.configure(state="normal") self.retry_btn.configure(state="normal") self.running = False self.status_var.set("就绪 | 重试任务完成") threading.Thread(target=worker, daemon=True).start() def clear_queue(self): """清除整个队列""" if not self.queue: return if messagebox.askyesno("确认", "确定要清空所有绑定任务吗?"): self.queue = [] self.task_ids = {} self.item_ids = {} for item in self.queue_tree.get_children(): self.queue_tree.delete(item) self.log_message("♻️ 绑定队列已清空") self.status_var.set("就绪") def export_log(self): """导出日志到文件""" try: timestamp = time.strftime("%Y%m%d-%H%M%S") filename = f"ip_mac_bind_{timestamp}.log" with open(filename, "w", encoding="utf-8") as f: f.write(self.log_area.get("1.0", tk.END)) self.log_message(f"📄 日志已导出到 {filename}") messagebox.showinfo("导出成功", f"日志已保存到 {filename}") except Exception as e: self.log_message(f"❌ 日志导出失败: {str(e)}") messagebox.showerror("导出失败", f"无法保存日志: {str(e)}") # ----------------------- 主程序 ----------------------- if __name__ == "__main__": root = tk.Tk() app = IPMacBindApp(root) # 居中显示窗口 window_width = 900 window_height = 750 screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() position_x = int((screen_width - window_width) / 2) position_y = int((screen_height - window_height) / 2) root.geometry(f"{window_width}x{window_height}+{position_x}+{position_y}") root.mainloop()
最新发布
07-16
import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import os import time import datetime import threading import win32com.client import win32api import win32con import pythoncom import traceback import re import subprocess import psutil import json import logging import shutil from logging.handlers import RotatingFileHandler class PC_DMIS_Automator: def __init__(self, root): self.root = root self.root.title("PC-DMIS 自动化工具 v1.0.7") self.root.geometry("1100x800") # 设置日志记录器 self.setup_logger() # 初始化变量 self.pcdmis_path = tk.StringVar() self.programs = [] # 存储程序路径和运行次数 self.pcdmis_version = "Unknown" self.running = False self.pcdmis_app = None # 用于存储PC-DMIS应用程序实例 self.config = self.load_config() # 加载配置 # 当前编辑的单元格信息 self.editing_cell = None self.edit_widget = None # 创建UI self.create_widgets() # 检测PC-DMIS self.detect_pcdmis() # 设置关闭事件 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) def setup_logger(self): """设置日志记录器""" self.logger = logging.getLogger("PC_DMIS_Automator") self.logger.setLevel(logging.DEBUG) # 创建日志目录 log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") if not os.path.exists(log_dir): os.makedirs(log_dir) # 文件日志处理器 log_file = os.path.join(log_dir, "pcdmis_automation.log") file_handler = RotatingFileHandler( log_file, maxBytes=5*1024*1024, backupCount=3 ) file_handler.setFormatter(logging.Formatter( &#39;%(asctime)s - %(name)s - %(levelname)s - %(message)s&#39; )) self.logger.addHandler(file_handler) # 控制台日志处理器 console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter( &#39;%(asctime)s - %(levelname)s - %(message)s&#39; )) self.logger.addHandler(console_handler) self.logger.info("PC-DMIS自动化工具启动") def load_config(self): """加载配置文件""" config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") default_config = { "default_path": "", "recent_programs": [], "run_count": 1, "log_level": "INFO", "timeout": 7200, # 2小时超时 "auto_save": True, "auto_report": True, "auto_backup": True, "use_excel_report": False, "excel_template": "", "email_notifications": False, "email_recipients": "", "email_server": "" } try: if os.path.exists(config_path): with open(config_path, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: return json.load(f) except Exception as e: self.logger.error(f"加载配置文件失败: {str(e)}") return default_config def save_config(self): """保存配置文件""" config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") try: self.config["recent_programs"] = [prog[0] for prog in self.programs][:10] # 保存最近10个程序 with open(config_path, &#39;w&#39;, encoding=&#39;utf-8&#39;) as f: json.dump(self.config, f, indent=4) except Exception as e: self.logger.error(f"保存配置文件失败: {str(e)}") def create_widgets(self): # 创建主框架 main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # PC-DMIS路径设置 path_frame = ttk.LabelFrame(main_frame, text="PC-DMIS设置") path_frame.pack(fill=tk.X, padx=5, pady=5) # 版本信息 version_frame = ttk.Frame(path_frame) version_frame.pack(fill=tk.X, padx=5, pady=2) ttk.Label(version_frame, text="检测到版本:").pack(side=tk.LEFT, padx=5) self.version_label = ttk.Label(version_frame, text=self.pcdmis_version, foreground="blue") self.version_label.pack(side=tk.LEFT, padx=5) # 状态指示灯 self.status_indicator = tk.Canvas(version_frame, width=20, height=20, bg="gray") self.status_indicator.pack(side=tk.RIGHT, padx=10) self.draw_status_indicator("gray") # 路径选择 path_frame_inner = ttk.Frame(path_frame) path_frame_inner.pack(fill=tk.X, padx=5, pady=2) ttk.Label(path_frame_inner, text="PC-DMIS路径:").pack(side=tk.LEFT, padx=5) path_entry = ttk.Entry(path_frame_inner, textvariable=self.pcdmis_path, width=70) path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) ttk.Button(path_frame_inner, text="浏览", command=self.browse_pcdmis).pack(side=tk.LEFT, padx=5) # 程序文件选择 program_frame = ttk.LabelFrame(main_frame, text="PC-DMIS程序管理") program_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 程序列表表格 columns = ("序号", "程序文件", "运行次数", "状态", "X", "↑", "↓") self.program_tree = ttk.Treeview( program_frame, columns=columns, show="headings", selectmode="browse", height=12 ) # 设置列宽 col_widths = [50, 450, 80, 100, 40, 40, 40] for col, width in zip(columns, col_widths): self.program_tree.column(col, width=width, anchor=tk.CENTER) # 设置列标题 for col in columns: self.program_tree.heading(col, text=col) # 添加滚动条 scrollbar = ttk.Scrollbar(program_frame, orient=tk.VERTICAL, command=self.program_tree.yview) self.program_tree.configure(yscroll=scrollbar.set) self.program_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 添加"+"行用于添加新程序 self.add_plus_row() # 控制面板 - 垂直排列设置项 settings_frame = ttk.Frame(program_frame) settings_frame.pack(fill=tk.X, padx=5, pady=5) # 运行次数编辑 - 第一行 run_count_frame = ttk.Frame(settings_frame) run_count_frame.pack(fill=tk.X, padx=5, pady=2) ttk.Label(run_count_frame, text="运行次数:").pack(side=tk.LEFT, padx=5) self.run_count_var = tk.IntVar(value=self.config.get("run_count", 1)) run_count_spin = ttk.Spinbox(run_count_frame, from_=1, to=100, textvariable=self.run_count_var, width=5) run_count_spin.pack(side=tk.LEFT, padx=5) # 超时设置 - 第二行(在运行次数下方) timeout_frame = ttk.Frame(settings_frame) timeout_frame.pack(fill=tk.X, padx=5, pady=2) ttk.Label(timeout_frame, text="超时时间(分钟):").pack(side=tk.LEFT, padx=5) self.timeout_var = tk.IntVar(value=self.config.get("timeout", 120) // 60) timeout_spin = ttk.Spinbox(timeout_frame, from_=1, to=360, textvariable=self.timeout_var, width=5) timeout_spin.pack(side=tk.LEFT, padx=5) # 高级选项 - 第三行 advanced_frame = ttk.Frame(settings_frame) advanced_frame.pack(fill=tk.X, padx=5, pady=2) # 自动备份 self.auto_backup_var = tk.BooleanVar(value=self.config.get("auto_backup", True)) backup_check = ttk.Checkbutton(advanced_frame, text="自动备份", variable=self.auto_backup_var) backup_check.pack(side=tk.LEFT, padx=5) # 自动报告 self.auto_report_var = tk.BooleanVar(value=self.config.get("auto_report", True)) report_check = ttk.Checkbutton(advanced_frame, text="生成报告", variable=self.auto_report_var) report_check.pack(side=tk.LEFT, padx=5) # Excel报告 self.excel_report_var = tk.BooleanVar(value=self.config.get("use_excel_report", False)) excel_check = ttk.Checkbutton(advanced_frame, text="使用Excel报告", variable=self.excel_report_var) excel_check.pack(side=tk.LEFT, padx=5) # 日志框 log_frame = ttk.LabelFrame(main_frame, text="运行日志") log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.log_text = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, height=12) self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.log_text.config(state=tk.DISABLED) # 添加日志级别标签 log_level_frame = ttk.Frame(log_frame) log_level_frame.pack(fill=tk.X, padx=5, pady=2) ttk.Label(log_level_frame, text="日志级别:").pack(side=tk.LEFT, padx=5) self.log_level = tk.StringVar(value=self.config.get("log_level", "INFO")) levels = [("详细", "DEBUG"), ("一般", "INFO"), ("警告", "WARNING"), ("错误", "ERROR")] for text, value in levels: ttk.Radiobutton(log_level_frame, text=text, variable=self.log_level, value=value, command=self.update_log_level).pack(side=tk.LEFT, padx=5) # 进度条 progress_frame = ttk.Frame(main_frame) progress_frame.pack(fill=tk.X, padx=5, pady=5) self.progress_label = ttk.Label(progress_frame, text="就绪") self.progress_label.pack(side=tk.LEFT, padx=5) self.progress = ttk.Progressbar(progress_frame, orient=tk.HORIZONTAL, length=700, mode=&#39;determinate&#39;) self.progress.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) # 按钮 button_frame = ttk.Frame(main_frame) button_frame.pack(fill=tk.X, padx=5, pady=5) # 左侧按钮组 left_btn_frame = ttk.Frame(button_frame) left_btn_frame.pack(side=tk.LEFT, fill=tk.X) ttk.Button(left_btn_frame, text="清除日志", command=self.clear_log).pack(side=tk.LEFT, padx=5) ttk.Button(left_btn_frame, text="导出日志", command=self.export_log).pack(side=tk.LEFT, padx=5) ttk.Button(left_btn_frame, text="保存配置", command=self.save_config).pack(side=tk.LEFT, padx=5) # 右侧按钮组 right_btn_frame = ttk.Frame(button_frame) right_btn_frame.pack(side=tk.RIGHT, fill=tk.X) self.start_button = ttk.Button(right_btn_frame, text="开始执行", command=self.start_execution) self.start_button.pack(side=tk.RIGHT, padx=5) self.stop_button = ttk.Button(right_btn_frame, text="停止", command=self.stop_execution, state=tk.DISABLED) self.stop_button.pack(side=tk.RIGHT, padx=5) # 绑定事件 self.program_tree.bind("<ButtonRelease-1>", self.on_tree_click) self.program_tree.bind("<Double-1>", self.on_double_click) # 状态栏 self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN, anchor=tk.W) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 加载最近使用的程序 self.load_recent_programs() # 创建用于编辑的Spinbox(初始隐藏) self.spinbox = ttk.Spinbox(self.program_tree, from_=1, to=100, width=5) self.spinbox.bind("<Return>", self.save_edit) self.spinbox.bind("<FocusOut>", self.save_edit) self.spinbox.place_forget() # 初始隐藏 def update_log_level(self): """更新日志级别并保存配置""" self.config["log_level"] = self.log_level.get() self.save_config() def load_recent_programs(self): """加载最近使用的程序""" for program in self.config.get("recent_programs", []): if os.path.exists(program): self.add_program([program]) def draw_status_indicator(self, color): """绘制状态指示灯""" self.status_indicator.delete("all") self.status_indicator.create_oval(2, 2, 18, 18, fill=color, outline="black") def add_plus_row(self): """添加&#39;+&#39;行用于添加新程序""" if "plus_row" not in self.program_tree.get_children(): self.program_tree.insert("", "end", iid="plus_row", values=("+", "", "", "", "", "", "")) def detect_pcdmis(self): """尝试自动检测PC-DMIS安装路径和版本""" try: # 尝试通过COM获取已运行的PC-DMIS实例 pythoncom.CoInitialize() app = win32com.client.GetActiveObject("PC-DMIS.Application") self.pcdmis_path.set(app.FullName) self.pcdmis_version = f"{app.Version} (已运行)" self.version_label.config(text=self.pcdmis_version) self.draw_status_indicator("green") self.log_message("INFO: 已连接到正在运行的PC-DMIS实例", "INFO") return except Exception as e: self.log_message(f"DEBUG: 无法通过COM获取PC-DMIS实例: {str(e)}", "DEBUG") # 尝试从注册表获取安装路径 reg_paths = [ r"SOFTWARE\Hexagon Metrology\PC-DMIS", r"SOFTWARE\WOW6432Node\Hexagon Metrology\PC-DMIS" # 64位系统 ] for reg_path in reg_paths: try: key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, reg_path, 0, win32con.KEY_READ) path, _ = win32api.RegQueryValueEx(key, "InstallationDirectory") exe_path = os.path.join(path, "PCDMIS.exe") if os.path.exists(exe_path): self.pcdmis_path.set(exe_path) # 尝试获取版本信息 try: info = win32api.GetFileVersionInfo(exe_path, "\\") version = f"{info[&#39;FileVersionMS&#39;]//65536}.{info[&#39;FileVersionMS&#39;] % 65536}.{info[&#39;FileVersionLS&#39;]//65536}" self.pcdmis_version = version self.log_message(f"INFO: 从注册表检测到PC-DMIS: {version}", "INFO") except Exception as e: self.pcdmis_version = "检测到 (未知版本)" self.log_message(f"WARNING: 无法获取版本信息: {str(e)}", "WARNING") self.version_label.config(text=self.pcdmis_version) self.draw_status_indicator("yellow") return except Exception as e: self.log_message(f"DEBUG: 无法从注册表获取PC-DMIS路径 ({reg_path}): {str(e)}", "DEBUG") # 如果所有方法都失败 self.log_message("WARNING: 无法自动检测PC-DMIS安装路径,请手动选择", "WARNING") self.draw_status_indicator("red") def browse_pcdmis(self): path = filedialog.askopenfilename( title="选择PC-DMIS可执行文件", filetypes=[("可执行文件", "*.exe"), ("所有文件", "*.*")] ) if path: self.pcdmis_path.set(path) try: # 尝试获取版本信息 info = win32api.GetFileVersionInfo(path, "\\") version = f"{info[&#39;FileVersionMS&#39;]//65536}.{info[&#39;FileVersionMS&#39;] % 65536}.{info[&#39;FileVersionLS&#39;]//65536}" self.pcdmis_version = version self.version_label.config(text=self.pcdmis_version) self.draw_status_indicator("green") self.log_message(f"INFO: 已选择PC-DMIS: {version}", "INFO") except Exception as e: self.pcdmis_version = "用户选择" self.version_label.config(text=self.pcdmis_version) self.log_message(f"WARNING: 无法获取版本信息: {str(e)}", "WARNING") self.draw_status_indicator("yellow") # 保存配置 self.config["default_path"] = path self.save_config() def add_program(self, files=None): if not files: files = filedialog.askopenfilenames( title="选择PC-DMIS程序文件", filetypes=[("PC-DMIS文件", "*.prg;*.pcf;*.pgm;*.dmis;*.pcm"), ("所有文件", "*.*")] ) if not files: return for file in files: if any(file == item[0] for item in self.programs): self.log_message(f"INFO: 程序已存在,跳过: {os.path.basename(file)}", "INFO") continue # 跳过已存在的文件 self.programs.append((file, self.run_count_var.get())) row_id = len(self.programs) self.program_tree.insert("", row_id-1, iid=f"row_{row_id-1}", values=(row_id, os.path.basename(file), self.run_count_var.get(), "等待", "X", "↑", "↓")) self.log_message(f"INFO: 已添加程序: {os.path.basename(file)} (运行次数: {self.run_count_var.get()})", "INFO") self.add_plus_row() self.update_row_numbers() self.save_config() def on_tree_click(self, event): item = self.program_tree.identify_row(event.y) column = self.program_tree.identify_column(event.x) if item == "plus_row": self.add_program() return if item and item != "plus_row": try: row_idx = int(item.split("_")[1]) except (IndexError, ValueError): return # 无效的 item,跳过 if row_idx < 0 or row_idx >= len(self.programs): return # 索引超出范围 col_index = int(column[1:]) - 1 # 列索引从0开始 columns = ["序号", "程序文件", "运行次数", "状态", "X", "↑", "↓"] if col_index == 4: # 删除 self.remove_program(row_idx) elif col_index == 5 and row_idx > 0: # 上移 self.move_program(row_idx, -1) elif col_index == 6 and row_idx < len(self.programs) - 1: # 下移 self.move_program(row_idx, 1) def on_double_click(self, event): """处理双击事件,用于编辑运行次数""" region = self.program_tree.identify("region", event.x, event.y) if region == "cell": item = self.program_tree.identify_row(event.y) column = self.program_tree.identify_column(event.x) # 只允许编辑运行次数列(第3列) if column == "#3" and item != "plus_row": try: row_idx = int(item.split("_")[1]) except (IndexError, ValueError): return # 获取当前值 values = self.program_tree.item(item, "values") current_value = values[2] # 运行次数在第3列 # 获取单元格位置 bbox = self.program_tree.bbox(item, column) if bbox: # 创建并显示Spinbox self.spinbox.delete(0, tk.END) self.spinbox.insert(0, current_value) self.spinbox.place(x=bbox[0], y=bbox[1], width=bbox[2], height=bbox[3]) self.spinbox.focus_set() # 保存编辑信息 self.editing_cell = (item, row_idx) def save_edit(self, event): """保存编辑的运行次数""" if not self.editing_cell: return item, row_idx = self.editing_cell new_value = self.spinbox.get() try: # 转换为整数 new_count = int(new_value) if new_count < 1: new_count = 1 elif new_count > 100: new_count = 100 # 更新Treeview values = list(self.program_tree.item(item, "values")) values[2] = new_count self.program_tree.item(item, values=values) # 更新数据存储 if 0 <= row_idx < len(self.programs): self.programs[row_idx] = (self.programs[row_idx][0], new_count) self.log_message(f"INFO: 已更新程序运行次数: {os.path.basename(self.programs[row_idx][0])} -> {new_count}次", "INFO") # 保存配置 self.save_config() except ValueError: self.log_message("ERROR: 运行次数必须是整数", "ERROR") # 隐藏Spinbox并清除编辑状态 self.spinbox.place_forget() self.editing_cell = None def remove_program(self, idx): if 0 <= idx < len(self.programs): program_name = os.path.basename(self.programs[idx][0]) self.programs.pop(idx) self.program_tree.delete(f"row_{idx}") self.add_plus_row() self.update_row_numbers() self.log_message(f"INFO: 已移除程序: {program_name}", "INFO") self.save_config() def update_row_numbers(self): """更新所有行的序号(排除 &#39;+&#39; 行)""" children = self.program_tree.get_children() for i, child in enumerate(children): if child != "plus_row": values = list(self.program_tree.item(child, "values")) if values: # 确保有值 values[0] = i # 更新序号 self.program_tree.item(child, values=values) def move_program(self, idx, direction): if idx < 0 or idx >= len(self.programs): return new_idx = idx + direction if new_idx < 0 or new_idx >= len(self.programs): return # 交换数据 self.programs[idx], self.programs[new_idx] = self.programs[new_idx], self.programs[idx] # 清除 Treeview 中所有项(保留 "+" 行) self.delete_all_program_rows() # 重新生成所有程序行 for i, (file, count) in enumerate(self.programs): self.program_tree.insert("", i, iid=f"row_{i}", values=(i+1, os.path.basename(file), count, "等待", "X", "↑", "↓")) # 重新添加 "+" 行 self.add_plus_row() program_name = os.path.basename(self.programs[new_idx][0]) direction_str = "上移" if direction < 0 else "下移" self.log_message(f"INFO: 已将程序 &#39;{program_name}&#39; {direction_str}", "INFO") self.save_config() def delete_all_program_rows(self): """删除所有程序项(保留 &#39;+&#39; 行)""" for child in self.program_tree.get_children(): if child != "plus_row": self.program_tree.delete(child) def clear_log(self): """清除日志内容""" self.log_text.config(state=tk.NORMAL) self.log_text.delete(1.0, tk.END) self.log_text.config(state=tk.DISABLED) self.log_message("INFO: 日志已清除", "INFO") def export_log(self): """导出日志到文件""" file_path = filedialog.asksaveasfilename( title="导出日志文件", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")], defaultextension=".txt" ) if file_path: try: with open(file_path, "w", encoding="utf-8") as f: f.write(self.log_text.get(1.0, tk.END)) self.log_message(f"INFO: 日志已导出到: {file_path}", "INFO") except Exception as e: self.log_message(f"ERROR: 导出日志失败: {str(e)}", "ERROR") def log_message(self, message, level="INFO"): """线程安全的日志更新""" # 记录到文件日志 log_func = { "DEBUG": self.logger.debug, "INFO": self.logger.info, "WARNING": self.logger.warning, "ERROR": self.logger.error }.get(level, self.logger.info) log_func(message) # 根据日志级别设置决定是否显示在UI log_levels = {"DEBUG": 1, "INFO": 2, "WARNING": 3, "ERROR": 4} current_level = log_levels.get(self.log_level.get(), 2) msg_level = log_levels.get(level, 2) if msg_level < current_level: return def update_log(): self.log_text.config(state=tk.NORMAL) timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 根据日志级别设置颜色 tag = level.lower() self.log_text.tag_config(tag, foreground=self.get_log_color(level)) self.log_text.insert(tk.END, f"[{timestamp}] ", "timestamp") self.log_text.insert(tk.END, f"{level}: ", tag) self.log_text.insert(tk.END, f"{message}\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) # 更新状态栏 self.status_bar.config(text=f"{level}: {message}") self.root.after(0, update_log) def get_log_color(self, level): """获取日志级别对应的颜色""" colors = { "DEBUG": "gray", "INFO": "black", "WARNING": "orange", "ERROR": "red" } return colors.get(level, "black") def update_progress(self, current, total, message): """线程安全的进度条更新""" def update_progress_bar(): self.progress_label.config(text=message) if total > 0: progress_value = (current / total) * 100 self.progress[&#39;value&#39;] = progress_value self.root.after(0, update_progress_bar) def update_program_status(self, row_idx, status, color="black"): """更新程序状态""" def update_status(): children = self.program_tree.get_children() if row_idx < len(children) and children[row_idx] != "plus_row": values = list(self.program_tree.item(f"row_{row_idx}", "values")) if values and len(values) > 3: values[3] = status # 更新状态列 self.program_tree.item(f"row_{row_idx}", values=values, tags=(color,)) self.program_tree.tag_configure(color, foreground=color) self.root.after(0, update_status) def start_execution(self): if not self.pcdmis_path.get() or not os.path.exists(self.pcdmis_path.get()): messagebox.showerror("错误", "无效的PC-DMIS路径") return if not self.programs: messagebox.showerror("错误", "请至少添加一个程序文件") return # 计算总任务数 total_tasks = sum(count for _, count in self.programs) if total_tasks == 0: messagebox.showerror("错误", "总运行次数不能为零") return # 更新配置 self.config["run_count"] = self.run_count_var.get() self.config["timeout"] = self.timeout_var.get() * 60 # 转换为秒 self.config["auto_backup"] = self.auto_backup_var.get() self.config["auto_report"] = self.auto_report_var.get() self.config["use_excel_report"] = self.excel_report_var.get() self.save_config() # 禁用按钮防止重复点击 self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.running = True # 在新线程中执行任务 threading.Thread( target=self.execute_programs, args=(total_tasks,), daemon=True ).start() def stop_execution(self): if not self.running: return self.running = False self.log_message("WARNING: 正在停止执行...", "WARNING") # 尝试停止当前正在运行的程序 if self.pcdmis_app and hasattr(self.pcdmis_app, &#39;ActiveProgram&#39;): try: if self.pcdmis_app.ActiveProgram.IsRunning: self.pcdmis_app.ActiveProgram.StopExecution() self.log_message("INFO: 已发送停止命令", "INFO") except Exception as e: self.log_message(f"ERROR: 停止命令发送失败: {str(e)}", "ERROR") def execute_programs(self, total_tasks): """执行所有添加的测量程序""" pythoncom.CoInitialize() # 初始化COM线程 completed_tasks = 0 timeout = self.config.get("timeout", 7200) # 默认2小时超时 try: self.log_message("INFO: 正在初始化PC-DMIS连接...", "INFO") # 尝试连接到正在运行的PC-DMIS实例 try: self.pcdmis_app = win32com.client.GetActiveObject("PC-DMIS.Application") self.log_message(f"INFO: 已连接到正在运行的PC-DMIS (版本: {self.pcdmis_app.Version})", "INFO") self.pcdmis_version = self.pcdmis_app.Version except Exception as e: self.log_message(f"WARNING: 无法连接到已运行的PC-DMIS实例: {str(e)}", "WARNING") # 启动新实例 try: self.log_message("INFO: 正在启动PC-DMIS...", "INFO") self.pcdmis_app = win32com.client.Dispatch("PC-DMIS.Application") self.pcdmis_app.Visible = True self.pcdmis_version = self.pcdmis_app.Version # 等待应用程序启动 start_time = time.time() while not self.is_pcdmis_ready(): if time.time() - start_time > 30: # 30秒超时 raise TimeoutError("PC-DMIS启动超时") time.sleep(1) self.log_message(f"INFO: PC-DMIS已启动 (版本: {self.pcdmis_version})", "INFO") except Exception as e: self.log_message(f"ERROR: 无法启动PC-DMIS: {str(e)}", "ERROR") self.running = False self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self.progress[&#39;value&#39;] = 0 self.progress_label.config(text="就绪") return # 保存当前版本信息 self.version_label.config(text=self.pcdmis_version) # 执行所有程序 for idx, (program_path, run_count) in enumerate(self.programs): if not self.running: break program_name = os.path.basename(program_path) base_name, ext = os.path.splitext(program_name) self.update_program_status(idx, "准备中", "blue") # 创建结果目录 save_dir = os.path.join(os.path.dirname(program_path), "自动化结果") if not os.path.exists(save_dir): os.makedirs(save_dir) self.log_message(f"INFO: 已创建结果目录: {save_dir}", "INFO") # 如果是DMIS脚本,使用命令行执行 if program_path.lower().endswith((&#39;.dms&#39;, &#39;.dmis&#39;)): self.log_message(f"INFO: 检测到DMIS脚本: {program_name}", "INFO") for run_idx in range(1, run_count + 1): if not self.running: break completed_tasks += 1 status_msg = f"正在执行DMIS脚本: {program_name} (第{run_idx}/{run_count}次)" self.update_progress(completed_tasks, total_tasks, status_msg) self.update_program_status(idx, f"执行中({run_idx}/{run_count})", "orange") try: # 执行DMIS脚本 success = self.execute_dmis_script(program_path) if success: self.update_program_status(idx, f"完成({run_idx}/{run_count})", "green") else: self.update_program_status(idx, f"失败({run_idx}/{run_count})", "red") except Exception as e: self.log_message(f"ERROR: 执行DMIS脚本时出错: {str(e)}", "ERROR") self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG") self.update_program_status(idx, f"错误({run_idx}/{run_count})", "red") continue for run_idx in range(1, run_count + 1): if not self.running: break # 更新进度 completed_tasks += 1 status_msg = f"正在处理: {program_name} (第{run_idx}/{run_count}次运行)" self.update_progress(completed_tasks, total_tasks, status_msg) self.update_program_status(idx, f"运行中({run_idx}/{run_count})", "orange") try: # 备份程序文件 if self.config.get("auto_backup", True): backup_path = self.backup_program(program_path) if backup_path: self.log_message(f"INFO: 程序已备份到: {backup_path}", "INFO") # 打开程序 self.log_message(f"INFO: 正在打开程序: {program_name} (运行 {run_idx}/{run_count})", "INFO") # 关闭当前打开的程序(如果有) if self.pcdmis_app.Programs.Count > 0: try: self.pcdmis_app.ActiveProgram.Close(True) # True表示不保存 time.sleep(1) except Exception as e: self.log_message(f"WARNING: 关闭当前程序失败: {str(e)}", "WARNING") # 打开新程序 self.pcdmis_app.OpenProgram(program_path) # 等待程序加载 start_time = time.time() while not hasattr(self.pcdmis_app, &#39;ActiveProgram&#39;) or not self.pcdmis_app.ActiveProgram: if time.time() - start_time > 30: # 30秒超时 raise TimeoutError("程序加载超时") time.sleep(1) self.log_message("INFO: 程序已加载", "INFO") # 执行程序 active_program = self.pcdmis_app.ActiveProgram self.log_message("INFO: 开始执行测量程序...", "INFO") active_program.ExecuteProgram() # 等待执行完成 start_time = time.time() while active_program.IsExecuting: if not self.running: self.log_message("WARNING: 用户中断程序执行", "WARNING") active_program.StopExecution() raise Exception("用户中断") # 检查超时 if time.time() - start_time > timeout: self.log_message("ERROR: 程序执行超时", "ERROR") active_program.StopExecution() raise TimeoutError(f"程序执行超时 ({timeout}秒)") time.sleep(1) self.log_message("INFO: 程序执行完成", "INFO") # 生成检测报告 report_path = None if self.config.get("auto_report", True): self.log_message("INFO: 正在生成检测报告...", "INFO") # 根据版本选择报告类型 if self.config.get("use_excel_report") and self.pcdmis_version >= "2019": report_path = self.generate_excel_report(active_program) else: report_content = self.generate_text_report(active_program) # 保存文本报告 report_filename = f"{base_name}_{datetime.datetime.now().strftime(&#39;%Y%m%d_%H%M%S&#39;)}_{run_idx:03d}.txt" report_path = os.path.join(save_dir, report_filename) with open(report_path, "w", encoding="utf-8") as f: f.write(report_content) self.log_message(f"INFO: 文本报告已保存: {report_path}", "INFO") # 保存程序副本 if self.config.get("auto_save", True): new_filename = f"{base_name}_{datetime.datetime.now().strftime(&#39;%Y%m%d&#39;)}_{run_idx:03d}{ext}" new_path = os.path.join(save_dir, new_filename) self.log_message(f"INFO: 正在保存程序副本为: {new_filename}", "INFO") active_program.SaveAs(new_path) # 关闭当前程序 self.log_message("INFO: 关闭当前程序", "INFO") active_program.Close(False) # False表示不保存(因为我们已保存) time.sleep(1) self.update_program_status(idx, f"完成({run_idx}/{run_count})", "green") except pythoncom.com_error as e: # 处理COM错误 hr, msg, exc, arg = e.args self.log_message(f"COM错误: {msg} (错误代码: 0x{hr:X})", "ERROR") if hasattr(e, &#39;excepinfo&#39;) and len(e.excepinfo) > 5: self.log_message(f"PC-DMIS错误代码: {e.excepinfo[5]}", "ERROR") self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG") self.update_program_status(idx, f"COM错误({run_idx}/{run_count})", "red") except Exception as e: self.log_message(f"ERROR: 处理程序时出错: {str(e)}", "ERROR") self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG") self.update_program_status(idx, f"错误({run_idx}/{run_count})", "red") # 尝试恢复状态 try: if hasattr(self.pcdmis_app, &#39;ActiveProgram&#39;) and self.pcdmis_app.ActiveProgram: self.pcdmis_app.ActiveProgram.Close(False) except: pass continue if self.running: self.log_message("INFO: 所有任务已完成!", "INFO") self.update_program_status(idx, "全部完成", "green") else: self.log_message("WARNING: 执行被用户中断", "WARNING") except pythoncom.com_error as e: hr, msg, exc, arg = e.args self.log_message(f"严重COM错误: {msg} (错误代码: 0x{hr:X})", "ERROR") self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG") except Exception as e: self.log_message(f"ERROR: 严重错误: {str(e)}", "ERROR") self.log_message(f"DEBUG: {traceback.format_exc()}", "DEBUG") finally: # 清理 try: if self.pcdmis_app: # 关闭所有打开的程序 while self.pcdmis_app.Programs.Count > 0: try: self.pcdmis_app.Programs(1).Close(False) time.sleep(0.5) except: pass # 如果我们启动的实例,则退出应用程序 if not self.is_pcdmis_running_before(): self.log_message("INFO: 退出PC-DMIS应用程序", "INFO") self.pcdmis_app.Quit() except Exception as e: self.log_message(f"WARNING: 清理过程中出错: {str(e)}", "WARNING") # 确保释放COM对象 try: del self.pcdmis_app self.pcdmis_app = None except: pass pythoncom.CoUninitialize() # 恢复界面状态 self.running = False self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self.progress[&#39;value&#39;] = 0 self.progress_label.config(text="就绪") if completed_tasks == total_tasks: self.status_bar.config(text="所有任务已完成") elif completed_tasks > 0: self.status_bar.config(text=f"部分完成: {completed_tasks}/{total_tasks}") else: self.status_bar.config(text="任务失败") self.save_config() def is_pcdmis_ready(self): """检查PC-DMIS是否已准备就绪""" try: # 尝试获取版本信息作为就绪检查 _ = self.pcdmis_app.Version return True except: return False def is_pcdmis_running_before(self): """检查PC-DMIS是否在启动前已经在运行""" try: # 尝试连接到正在运行的实例 temp_app = win32com.client.GetActiveObject("PC-DMIS.Application") return True except: return False def backup_program(self, program_path): """自动备份程序文件""" try: backup_dir = os.path.join(os.path.dirname(program_path), "备份") os.makedirs(backup_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") backup_name = f"{os.path.basename(program_path)}_{timestamp}" backup_path = os.path.join(backup_dir, backup_name) shutil.copy2(program_path, backup_path) self.log_message(f"INFO: 程序已备份到: {backup_path}", "INFO") return backup_path except Exception as e: self.log_message(f"ERROR: 备份失败: {str(e)}", "ERROR") return None def execute_dmis_script(self, script_path): """通过命令行执行 DMIS 脚本""" try: if not os.path.exists(script_path): self.log_message(f"ERROR: DMIS 脚本不存在: {script_path}", "ERROR") return False cmd = f&#39;"{self.pcdmis_path.get()}" /RUN "{script_path}"&#39; self.log_message(f"INFO: 执行命令: {cmd}", "INFO") result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=3600) if result.returncode == 0: self.log_message(f"INFO: DMIS 脚本执行成功: {script_path}", "INFO") return True else: self.log_message(f"ERROR: DMIS 脚本执行失败: {result.stderr}", "ERROR") return False except subprocess.TimeoutExpired: self.log_message("ERROR: DMIS 脚本执行超时", "ERROR") return False except Exception as e: self.log_message(f"ERROR: 执行 DMIS 脚本时出错: {str(e)}", "ERROR") return False def generate_text_report(self, program): """生成文本格式报告""" try: report_content = f"PC-DMIS 检测报告\n生成时间: {datetime.datetime.now().strftime(&#39;%Y-%m-%d %H:%M:%S&#39;)}\n" report_content += f"程序名称: {program.Name}\n" report_content += f"零件编号: {program.PartNumber}\n" report_content += f"序列号: {program.SerialNumber}\n" report_content += f"操作员: {program.Operator}\n" report_content += f"测量设备: {program.MachineName}\n\n" report_content += "===== 测量结果摘要 =====\n" # 添加测量特征结果 for i in range(1, program.Features.Count + 1): feature = program.Features(i) report_content += f"特征: {feature.Name}\n" report_content += f"理论值: {feature.Nominal}\n" report_content += f"实测值: {feature.Actual}\n" report_content += f"偏差: {feature.Deviation}\n" report_content += f"公差: {feature.Tolerance}\n" report_content += f"状态: {&#39;合格&#39; if feature.WithinTol else &#39;超差&#39;}\n" report_content += "------------------------\n" report_content += "\n===== 详细信息请查看PC-DMIS报告文件 =====\n" return report_content except Exception as e: self.log_message(f"WARNING: 无法获取完整报告: {str(e)}", "WARNING") return f"PC-DMIS 检测报告\n程序: {program.Name}\n生成时间: {datetime.datetime.now()}\n错误: 无法获取完整报告详情" def generate_excel_report(self, program): """生成 Excel 格式检测报告""" try: # 添加 Excel 报告命令 excel_report = program.ReportCommands.Add("Excel") # 设置报告模板(如果提供) if self.config.get("excel_template"): excel_report.TemplatePath = self.config["excel_template"] # 设置输出路径 save_dir = os.path.join(os.path.dirname(program.FullName), "自动化报告") os.makedirs(save_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") report_path = os.path.join(save_dir, f"{program.Name}_{timestamp}.xlsx") excel_report.OutputPath = report_path # 执行报告导出 excel_report.Refresh() self.log_message(f"INFO: Excel 报告已导出: {report_path}", "INFO") return report_path except Exception as e: self.log_message(f"ERROR: Excel 报告导出失败: {str(e)}", "ERROR") return None def on_closing(self): """窗口关闭事件处理""" if self.running: if messagebox.askyesno("确认", "任务正在运行,确定要退出吗?"): self.stop_execution() self.root.destroy() else: self.root.destroy() self.save_config() if __name__ == "__main__": root = tk.Tk() app = PC_DMIS_Automator(root) root.mainloop() 分析一下我的这个python自动化PC DMIS的代码功能,检查问题,并修复问题,特别注意对弈pcdmis各版本的接口支持问题,修复后要自我验证,确保无异常和bug,提供修复后的完整代码
07-09
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using ESRI.ArcGIS.Carto; using ESRI.ArcGIS.Controls; using ESRI.ArcGIS.esriSystem; using ESRI.ArcGIS.Geometry; using ESRI.ArcGIS.Geodatabase; using ESRI.ArcGIS.Display; namespace WaterPipelineGIS2 { public partial class Form1 : Form { private IFeatureLayer _selectedFeatureLayer; private System.Drawing.Point _lastRightClickPosition; private enum SelectionMode { None, Rectangle, Circle, Polygon, Polyline } private SelectionMode currentMode = SelectionMode.None; private enum MeasureMode { None, Area, Length, Point } private MeasureMode currentMeasureMode = MeasureMode.None; private IGeometry tempGeometry; private IPointCollection pointCollection; private Label lblMeasureResult; private IGraphicsContainer _spatialSelectionGraphics; // 空间选择图形容器 private IGraphicsContainer _attributeHighlightGraphics; // 属性表高亮图形容器 public Form1() { InitializeComponent(); axMapControl1.OnMouseMove += new IMapControlEvents2_Ax_OnMouseMoveEventHandler(axMapControl1_OnMouseMove); } private void axMapControl1_OnMouseMove(object sender, IMapControlEvents2_OnMouseMoveEvent e) { // 获取地图坐标 double mapX = e.mapX; double mapY = e.mapY; // 格式化坐标显示(保留3位小数) lblCoordinate.Text = string.Format("X: {0:F3} Y: {1:F3}", mapX, mapY); // 立即刷新状态栏 statusStrip1.Refresh(); } private void toolStripMenuItem2_Click(object sender, EventArgs e) { using (var rotateForm = new RotateForm()) { if (rotateForm.ShowDialog() == DialogResult.OK) { try { axMapControl1.Rotation = rotateForm.RotationAngle; axMapControl1.ActiveView.Refresh(); // 可选:更新状态栏 lblCoordinate.Text += " 旋转角度:{rotateForm.RotationAngle}°"; } catch (Exception ex) { MessageBox.Show("旋转失败:{ex.Message}"); } } } } private void axTOCControl1_OnMouseDown(object sender, ITOCControlEvents_OnMouseDownEvent e) { if (e.button == 2) // 右键 { // 保存点击位置(控件坐标系) _lastRightClickPosition = new System.Drawing.Point(e.x, e.y); ITOCControl2 tocControl = (ITOCControl2)axTOCControl1.Object; // 修改点1:声明为接口类型并初始化为null IBasicMap basicMap = null; ILayer layer = null; // 修改点2:使用Type.Missing代替new object() object other = Type.Missing; object index = Type.Missing; esriTOCControlItem itemType = esriTOCControlItem.esriTOCControlItemNone; // 修改点3:正确传递ref参数 tocControl.HitTest(e.x, e.y, ref itemType, ref basicMap, ref layer, ref other, ref index); if (itemType == esriTOCControlItem.esriTOCControlItemLayer && layer != null) { contextMenuStripTOC.Show(axTOCControl1, e.x, e.y); } } } // 修改后(使用 MouseEventArgs) private void openAttributeTableToolStripMenuItem_Click(object sender, EventArgs e) { ITOCControl2 tocControl = (ITOCControl2)axTOCControl1.Object; IBasicMap basicMap = null; ILayer layer = null; object other = Type.Missing; object index = Type.Missing; esriTOCControlItem itemType = esriTOCControlItem.esriTOCControlItemNone; // 获取当前鼠标位置(屏幕坐标系) System.Drawing.Point screenPos = contextMenuStripTOC.PointToClient(Control.MousePosition); // 转换为控件坐标系 System.Drawing.Point controlPos = axTOCControl1.PointToClient(Control.MousePosition); tocControl.HitTest( controlPos.X, controlPos.Y, ref itemType, ref basicMap, ref layer, ref other, ref index ); IFeatureLayer featureLayer = layer as IFeatureLayer; if (featureLayer != null) { _selectedFeatureLayer = featureLayer; AttributeTableForm attrForm = new AttributeTableForm( _selectedFeatureLayer, this // 传递主窗体引用 ); attrForm.Show(); } } private void SetSelectionSymbol() { // 使用接口创建符号 ISimpleFillSymbol fillSymbol = new SimpleFillSymbol() as ISimpleFillSymbol; fillSymbol.Color = GetRgbColor(255, 0, 0); fillSymbol.Style = esriSimpleFillStyle.esriSFSSolid; ISimpleLineSymbol lineSymbol = new SimpleLineSymbol() as ISimpleLineSymbol; lineSymbol.Color = GetRgbColor(255, 255, 0); lineSymbol.Width = 2; fillSymbol.Outline = lineSymbol; // 设置渲染器 if (_selectedFeatureLayer != null) { IGeoFeatureLayer geoLayer = (IGeoFeatureLayer)_selectedFeatureLayer; ISimpleRenderer renderer = new SimpleRenderer() as ISimpleRenderer; renderer.Symbol = (ISymbol)fillSymbol; geoLayer.Renderer = (IFeatureRenderer)renderer; axMapControl1.ActiveView.Refresh(); } } private IRgbColor GetRgbColor(int r, int g, int b) { IRgbColor color = new RgbColor() as IRgbColor; // 正确方式 color.Red = r; color.Green = g; color.Blue = b; return color; } public void ActivateFeatureLayer(IFeatureLayer featureLayer) { if (featureLayer == null) return; // 1. 设置当前活动图层 _selectedFeatureLayer = featureLayer; // 2. 刷新地图显示(可选) axMapControl1.ActiveView.Refresh(); // 3. 更新TOC控件的选中状态(可选) axTOCControl1.Update(); } // 修改 HighlightAndZoomToFeature 方法实现 public void HighlightAndZoomToFeature(IFeatureLayer featureLayer, int oid) { try { if (featureLayer == null || featureLayer.FeatureClass == null) { MessageBox.Show("图层或要素类无效!"); return; } // 获取要素并检查有效性 IFeature feature = featureLayer.FeatureClass.GetFeature(oid); if (feature == null || feature.Shape == null) { MessageBox.Show("要素 OID {oid} 不存在或无几何!"); return; } IGeometry geometry = feature.Shape; IEnvelope envelope = geometry.Envelope; // 确保包络线有效 if (envelope.IsEmpty || envelope.Width == 0 || envelope.Height == 0) { envelope.Expand(10, 10, true); // 手动扩大点要素范围 } else { envelope.Expand(1.5, 1.5, true); } // 缩放到要素范围 axMapControl1.Extent = envelope; axMapControl1.ActiveView.ScreenDisplay.UpdateWindow(); // 高亮几何 HighlightGeometry(geometry); } catch (Exception ex) { MessageBox.Show("高亮要素失败: {ex.Message}"); } } public object _featureLayer { get; set; } // 矩形选择 private void btnRectSelect_Click(object sender, EventArgs e) { currentMode = SelectionMode.Rectangle; axMapControl1.CurrentTool = null; } // 圆形选择 private void btnCircleSelect_Click(object sender, EventArgs e) { currentMode = SelectionMode.Circle; axMapControl1.CurrentTool = null; } // 多边形选择 private void btnPolygonSelect_Click(object sender, EventArgs e) { currentMode = SelectionMode.Polygon; axMapControl1.CurrentTool = null; } // 折线选择 private void btnLineSelect_Click(object sender, EventArgs e) { currentMode = SelectionMode.Polyline; axMapControl1.CurrentTool = null; } // 地图鼠标按下事件 private void axMapControl1_OnMouseDown(object sender, IMapControlEvents2_OnMouseDownEvent e) { try { IGeometry geometry = null; switch (currentMode) { case SelectionMode.Rectangle: // 绘制矩形(返回的是IEnvelope) geometry = axMapControl1.TrackRectangle(); break; case SelectionMode.Circle: IEnvelope envelope = axMapControl1.TrackRectangle(); if (envelope.Width <= 0 || envelope.Height <= 0) { MessageBox.Show("请拖拽有效的矩形范围以创建圆形!"); return; } // 创建圆心点 IPoint center = new PointClass(); center.PutCoords( (envelope.XMin + envelope.XMax) / 2, (envelope.YMin + envelope.YMax) / 2 ); // 计算半径(取矩形宽高的平均值) double radius = Math.Max(envelope.Width, envelope.Height) / 2; // 创建圆形几何 ICircularArc circularArc = new CircularArcClass(); IConstructCircularArc constructArc = (IConstructCircularArc)circularArc; constructArc.ConstructCircle(center, radius, true); // 将圆弧转换为多边形 ISegmentCollection segColl = new PolygonClass(); segColl.AddSegment((ISegment)circularArc); geometry = (IGeometry)segColl; geometry.SpatialReference = axMapControl1.SpatialReference; break; case SelectionMode.Polygon: // 绘制多边形 geometry = axMapControl1.TrackPolygon(); break; case SelectionMode.Polyline: // 绘制折线 geometry = axMapControl1.TrackLine(); break; } if (geometry != null) { PerformSpatialSelection(geometry); HighlightGeometry(geometry); } } catch (Exception ex) { MessageBox.Show("操作失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // 执行空间选择 private void PerformSpatialSelection(IGeometry geometry) { try { IMap map = axMapControl1.Map; if (map == null) return; map.ClearSelection(); for (int i = 0; i < map.LayerCount; i++) { IFeatureLayer featureLayer = map.get_Layer(i) as IFeatureLayer; if (featureLayer == null || !featureLayer.Valid || featureLayer.FeatureClass == null) continue; // 检查几何类型是否支持空间查询 if (geometry.GeometryType == esriGeometryType.esriGeometryPoint && featureLayer.FeatureClass.ShapeType != esriGeometryType.esriGeometryPoint) continue; ISpatialFilter filter = new SpatialFilterClass { Geometry = geometry, GeometryField = featureLayer.FeatureClass.ShapeFieldName, SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects }; IFeatureSelection selection = (IFeatureSelection)featureLayer; selection.SelectFeatures(filter, esriSelectionResultEnum.esriSelectionResultNew, false); } axMapControl1.Refresh(esriViewDrawPhase.esriViewGeoSelection, null, null); } catch (Exception ex) { MessageBox.Show("空间查询失败: {ex.Message}"); } } // 高亮显示选择图形(修复版) private void HighlightGeometry(IGeometry geometry) { if (geometry == null || geometry.IsEmpty) { MessageBox.Show("几何无效!"); return; } IGraphicsContainer graphicsContainer = axMapControl1.Map as IGraphicsContainer; graphicsContainer.DeleteAllElements(); // 清除旧图形 // 创建符号和元素 IElement element = null; ISymbol symbol = null; switch (geometry.GeometryType) { case esriGeometryType.esriGeometryPolygon: case esriGeometryType.esriGeometryEnvelope: ISimpleFillSymbol fillSymbol = new SimpleFillSymbolClass { Color = GetRgbColor(255, 0, 0, 80), Style = esriSimpleFillStyle.esriSFSSolid }; fillSymbol.Outline = new SimpleLineSymbolClass { Color = GetRgbColor(255, 0, 0), Width = 2 }; symbol = (ISymbol)fillSymbol; element = new PolygonElementClass(); break; case esriGeometryType.esriGeometryPolyline: ISimpleLineSymbol lineSymbol = new SimpleLineSymbolClass { Color = GetRgbColor(255, 0, 0), Width = 3 }; symbol = (ISymbol)lineSymbol; element = new LineElementClass(); break; case esriGeometryType.esriGeometryPoint: ISimpleMarkerSymbol markerSymbol = new SimpleMarkerSymbolClass { Color = GetRgbColor(255, 0, 0), Size = 12, Style = esriSimpleMarkerStyle.esriSMSCircle }; symbol = (ISymbol)markerSymbol; element = new MarkerElementClass(); break; } if (element != null && symbol != null) { element.Geometry = geometry; graphicsContainer.AddElement(element, 0); axMapControl1.ActiveView.PartialRefresh(esriViewDrawPhase.esriViewGraphics, null, null); } } private IColor GetRgbColor(int p, int p_2, int p_3, int p_4) { throw new NotImplementedException(); } // 创建颜色对象 private IRgbColor GetRGBColor(int r, int g, int b, int alpha = 255) { IRgbColor color = new RgbColorClass(); color.Red = r; color.Green = g; color.Blue = b; color.Transparency = (byte)(255 - alpha); return color; } // 清除选择 private void btnClearSelection_Click(object sender, EventArgs e) { axMapControl1.Map.ClearSelection(); IGraphicsContainer graphicsContainer = axMapControl1.Map as IGraphicsContainer; graphicsContainer.DeleteAllElements(); axMapControl1.Refresh(); } // 新增双击事件处理 private void axMapControl1_OnDoubleClick(object sender, IMapControlEvents2_OnDoubleClickEvent e) { if (currentMode != SelectionMode.None) { currentMode = SelectionMode.None; axMapControl1.CurrentTool = null; axMapControl1.MousePointer = esriControlsMousePointer.esriPointerDefault; } } // 添加量测按钮点击事件 private void btnMeasureArea_Click(object sender, EventArgs e) { ResetMeasureMode(MeasureMode.Area); lblMeasureResult.Text = "状态:绘制多边形(双击结束)"; } private void btnMeasureLength_Click(object sender, EventArgs e) { ResetMeasureMode(MeasureMode.Length); lblMeasureResult.Text = "状态:绘制折线(双击结束)"; } private void btnMeasurePoint_Click(object sender, EventArgs e) { ResetMeasureMode(MeasureMode.Point); lblMeasureResult.Text = "状态:移动鼠标查看坐标"; } private void ResetMeasureMode(MeasureMode mode) { currentMeasureMode = mode; ClearTempGraphics(); axMapControl1.CurrentTool = null; axMapControl1.MousePointer = esriControlsMousePointer.esriPointerCrosshair; } private void ClearTempGraphics() { throw new NotImplementedException(); } // 修改后的地图鼠标事件处理 private void axMapControl1_OnMouseDown2(object sender, IMapControlEvents2_OnMouseDownEvent e) { try { if (currentMeasureMode == MeasureMode.None) return; IPoint currentPoint = axMapControl1.ActiveView.ScreenDisplay.DisplayTransformation.ToMapPoint(e.x, e.y); switch (currentMeasureMode) { case MeasureMode.Area: case MeasureMode.Length: if (pointCollection == null) { // 🟢 根据模式创建不同几何类型 pointCollection = currentMeasureMode == MeasureMode.Area ? (IPointCollection)new PolygonClass() : (IPointCollection)new PolylineClass(); pointCollection.AddPoint(currentPoint); } pointCollection.AddPoint(currentPoint); DrawTempGeometry(); break; } } catch (Exception ex) { MessageBox.Show("操作错误:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void DrawTempGeometry() { throw new NotImplementedException(); } } }using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using ESRI.ArcGIS.Carto; using ESRI.ArcGIS.Display; using ESRI.ArcGIS.Controls; using ESRI.ArcGIS.Geodatabase; namespace WaterPipelineGIS2 { public partial class AttributeTableForm : Form { private IFeatureLayer _featureLayer; private Form1 _mainForm; // 主窗体引用 public AttributeTableForm(IFeatureLayer featureLayer, Form1 mainForm) { InitializeComponent(); _featureLayer = featureLayer; _mainForm = mainForm; // 绑定数据 BindData(); // 绑定行选择事件 dgvAttributes.SelectionChanged += DgvAttributes_SelectionChanged; // 添加行双击事件 dgvAttributes.CellDoubleClick += (s, args) => { if (args.RowIndex >= 0) { int oid = Convert.ToInt32(dgvAttributes.Rows[args.RowIndex].Cells[0].Value); _mainForm.ActivateFeatureLayer(_featureLayer); _mainForm.HighlightAndZoomToFeature(_featureLayer, oid); } }; } private void DgvAttributes_SelectionChanged(object sender, EventArgs e) { if (dgvAttributes.SelectedRows.Count == 0) return; // 获取当前行的 OID int oid = Convert.ToInt32(dgvAttributes.SelectedRows[0].Cells[0].Value); // 激活主窗体的图层 _mainForm.ActivateFeatureLayer(_featureLayer); // 调用主窗体的高亮方法(传递当前图层) _mainForm.HighlightAndZoomToFeature(_featureLayer, oid); } private void BindData() { IFeatureCursor cursor = _featureLayer.FeatureClass.Search(null, true); DataTable dataTable = new DataTable(); // 添加 OID 字段(关键修改:使用要素类的 OID 字段名) dataTable.Columns.Add(_featureLayer.FeatureClass.OIDFieldName, typeof(int)); // 自动获取系统 OID 字段名 // 添加其他字段 for (int i = 0; i < cursor.Fields.FieldCount; i++) { IField field = cursor.Fields.get_Field(i); if (field.Type == esriFieldType.esriFieldTypeOID) continue; // 跳过已添加的 OID 字段 dataTable.Columns.Add(field.Name, GetFieldType(field.Type)); } // 填充数据 IFeature feature; while ((feature = cursor.NextFeature()) != null) { DataRow row = dataTable.NewRow(); row[0] = feature.OID; // 直接使用 OID 属性 for (int i = 0; i < cursor.Fields.FieldCount; i++) { IField field = cursor.Fields.get_Field(i); if (field.Type == esriFieldType.esriFieldTypeOID) continue; row[field.Name] = feature.get_Value(i) ?? DBNull.Value; } dataTable.Rows.Add(row); } dgvAttributes.DataSource = dataTable; } // 辅助方法:将 ArcGIS 字段类型转换为 .NET 类型 private Type GetFieldType(esriFieldType fieldType) { switch (fieldType) { case esriFieldType.esriFieldTypeString: return typeof(string); case esriFieldType.esriFieldTypeInteger: return typeof(int); case esriFieldType.esriFieldTypeDouble: return typeof(double); case esriFieldType.esriFieldTypeDate: return typeof(DateTime); default: return typeof(string); } } } } 此代码能进行空间选择,但是绘制后会弹出报错窗口,且之前绘制后会暂时留下的红色区域不见了,如何解决
07-13
import struct import os import tkinter as tk from tkinter import ttk, messagebox, simpledialog, filedialog, scrolledtext import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import platform import sys import datetime import math import hashlib import re import binascii class FAT32Parser: def __init__(self, device_path, readonly=True): self.device_path = device_path self.readonly = readonly self.sector_size = 512 self.cluster_size = 0 self.fat_start = 0 self.data_start = 0 self.root_cluster = 0 self.fat = [] self.free_clusters = [] self.total_clusters = 0 try: # 打开设备 mode = &#39;rb&#39; if readonly else &#39;r+b&#39; self.fd = open(device_path, mode) except Exception as e: raise ValueError(f"无法打开设备: {e}") try: # 读取引导扇区 self.fd.seek(0) boot_sector = self.fd.read(512) # 检查FAT32签名 if len(boot_sector) < 512 or boot_sector[510] != 0x55 or boot_sector[511] != 0xAA: raise ValueError("无效的引导扇区签名 - 可能不是FAT32格式") # 解析关键参数 self.sectors_per_cluster = boot_sector[13] if self.sectors_per_cluster not in [1, 2, 4, 8, 16, 32, 64, 128]: raise ValueError("无效的每簇扇区数") self.reserved_sectors = struct.unpack(&#39;<H&#39;, boot_sector[14:16])[0] self.num_fats = boot_sector[16] self.sectors_per_fat = struct.unpack(&#39;<I&#39;, boot_sector[36:40])[0] self.root_cluster = struct.unpack(&#39;<I&#39;, boot_sector[44:48])[0] self.total_sectors = struct.unpack(&#39;<I&#39;, boot_sector[32:36])[0] or struct.unpack(&#39;<I&#39;, boot_sector[40:44])[0] # 计算关键位置 self.cluster_size = self.sectors_per_cluster * self.sector_size self.fat_start = self.reserved_sectors * self.sector_size self.data_start = self.fat_start + (self.num_fats * self.sectors_per_fat * self.sector_size) # 计算总簇数 self.total_clusters = (self.total_sectors - self.reserved_sectors - self.num_fats * self.sectors_per_fat) // self.sectors_per_cluster # 加载FAT表 self._load_fat_table() # 检查根簇号是否有效 if self.root_cluster < 2 or self.root_cluster >= len(self.fat): raise ValueError(f"无效的根簇号: {self.root_cluster}") # 扫描空闲簇 self._find_free_clusters() # 保存引导扇区信息 self.boot_sector_info = self._parse_boot_sector(boot_sector) except Exception as e: self.fd.close() raise e def _parse_boot_sector(self, boot_sector): """解析引导扇区信息""" info = {} info[&#39;OEM Name&#39;] = boot_sector[3:11].decode(&#39;ascii&#39;, errors=&#39;replace&#39;).strip() info[&#39;Bytes Per Sector&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[11:13])[0] info[&#39;Sectors Per Cluster&#39;] = boot_sector[13] info[&#39;Reserved Sectors&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[14:16])[0] info[&#39;Number of FATs&#39;] = boot_sector[16] info[&#39;Root Entries&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[17:19])[0] # FAT16 only info[&#39;Total Sectors 16&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[19:21])[0] # FAT16 only info[&#39;Media Descriptor&#39;] = hex(boot_sector[21]) info[&#39;Sectors Per FAT 16&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[22:24])[0] # FAT16 only info[&#39;Sectors Per Track&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[24:26])[0] info[&#39;Number of Heads&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[26:28])[0] info[&#39;Hidden Sectors&#39;] = struct.unpack(&#39;<I&#39;, boot_sector[28:32])[0] info[&#39;Total Sectors 32&#39;] = struct.unpack(&#39;<I&#39;, boot_sector[32:36])[0] # FAT32 specific info[&#39;Sectors Per FAT&#39;] = struct.unpack(&#39;<I&#39;, boot_sector[36:40])[0] info[&#39;Flags&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[40:42])[0] info[&#39;FAT Version&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[42:44])[0] info[&#39;Root Directory Cluster&#39;] = struct.unpack(&#39;<I&#39;, boot_sector[44:48])[0] info[&#39;FSInfo Sector&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[48:50])[0] info[&#39;Backup Boot Sector&#39;] = struct.unpack(&#39;<H&#39;, boot_sector[50:52])[0] info[&#39;Volume Label&#39;] = boot_sector[71:82].decode(&#39;ascii&#39;, errors=&#39;replace&#39;).strip() info[&#39;File System Type&#39;] = boot_sector[82:90].decode(&#39;ascii&#39;, errors=&#39;replace&#39;).strip() return info def _load_fat_table(self): """加载FAT表到内存""" self.fd.seek(self.fat_start) fat_size = self.sectors_per_fat * self.sector_size fat_data = self.fd.read(fat_size) if len(fat_data) != fat_size: raise ValueError("读取FAT表失败") # 解析FAT32表项 (每4字节一个簇) self.fat = [] for i in range(0, len(fat_data), 4): # 只取低28位(FAT32实际用28位) entry = struct.unpack(&#39;<I&#39;, fat_data[i:i+4])[0] & 0x0FFFFFFF self.fat.append(entry) def _find_free_clusters(self): """查找所有空闲簇""" self.free_clusters = [] for cluster_idx in range(2, len(self.fat)): # 簇0和1保留 if self.fat[cluster_idx] == 0: self.free_clusters.append(cluster_idx) def get_cluster_chain(self, start_cluster): """获取文件的簇链""" if start_cluster < 2 or start_cluster >= len(self.fat): return [] # 无效簇号 chain = [] current = start_cluster visited = set() # 追踪簇链直到结束 while current < 0x0FFFFFF8: # 文件结束标记 if current in visited: messagebox.showwarning("警告", f"发现循环簇链!簇 {current} 已被访问过") break # 防止无限循环 if current >= len(self.fat): messagebox.showwarning("警告", f"簇链越界!簇 {current} 超出FAT表范围") break # 越界保护 chain.append(current) visited.add(current) next_cluster = self.fat[current] if next_cluster == 0 or next_cluster >= 0x0FFFFFF8: break current = next_cluster return chain def get_fat_chain(self, start_cluster): """获取文件的FAT链(包含FAT表值)""" chain = self.get_cluster_chain(start_cluster) fat_chain = [] for cluster in chain: if cluster < len(self.fat): fat_chain.append((cluster, self.fat[cluster])) return fat_chain def read_directory(self, cluster): """读取目录内容""" entries = [] chain = self.get_cluster_chain(cluster) if not chain: return entries # 长文件名缓存 lfn_parts = [] for c in chain: # 计算簇对应的扇区 sector_offset = self.data_start + (c - 2) * self.cluster_size if sector_offset < 0 or sector_offset > self.get_disk_size(): break self.fd.seek(sector_offset) data = self.fd.read(self.cluster_size) if len(data) < self.cluster_size: break # 读取不完整 # 解析每个目录项(32字节) for i in range(0, len(data), 32): entry = data[i:i+32] if len(entry) < 32: continue if entry[0] == 0x00: # 空闲 lfn_parts = [] # 重置长文件名缓存 continue if entry[0] == 0xE5: # 删除 lfn_parts = [] # 重置长文件名缓存 continue attr = entry[11] if attr == 0x0F: # 长文件名条目 # 解析长文件名片段 seq = entry[0] name_part = entry[1:11] + entry[14:26] + entry[28:32] # 移除尾部的0x00 name_part = name_part.split(b&#39;\x00&#39;)[0] try: name_part = name_part.decode(&#39;utf-16le&#39;, errors=&#39;ignore&#39;) except: name_part = &#39;?&#39; lfn_parts.append((seq, name_part)) continue # 短文件名条目 name = entry[0:8].decode(&#39;latin-1&#39;, errors=&#39;ignore&#39;).strip() ext = entry[8:11].decode(&#39;latin-1&#39;, errors=&#39;ignore&#39;).strip() fullname = name + (&#39;.&#39; + ext if ext else &#39;&#39;) # 如果有长文件名,使用它 if lfn_parts: # 排序并组合长文件名片段 lfn_parts.sort(key=lambda x: x[0]) long_name = &#39;&#39;.join(part for _, part in reversed(lfn_parts)) fullname = long_name.strip() lfn_parts = [] # 解析属性 is_dir = bool(attr & 0x10) is_volume = bool(attr & 0x08) is_hidden = bool(attr & 0x02) is_system = bool(attr & 0x04) is_archive = bool(attr & 0x20) # 获取起始簇号 start_cluster_hi = struct.unpack(&#39;<H&#39;, entry[20:22])[0] start_cluster_lo = struct.unpack(&#39;<H&#39;, entry[26:28])[0] start_cluster = (start_cluster_hi << 16) | start_cluster_lo # 文件大小 size = struct.unpack(&#39;<I&#39;, entry[28:32])[0] # 解析时间日期 create_time = struct.unpack(&#39;<H&#39;, entry[14:16])[0] create_date = struct.unpack(&#39;<H&#39;, entry[16:18])[0] mod_time = struct.unpack(&#39;<H&#39;, entry[22:24])[0] mod_date = struct.unpack(&#39;<H&#39;, entry[24:26])[0] # 跳过卷标 if is_volume: continue # 添加所有条目 entries.append({ &#39;name&#39;: fullname, &#39;is_dir&#39;: is_dir, &#39;is_hidden&#39;: is_hidden, &#39;is_system&#39;: is_system, &#39;is_archive&#39;: is_archive, &#39;start_cluster&#39;: start_cluster, &#39;size&#39;: size, &#39;create_time&#39;: self._parse_dos_datetime(create_date, create_time), &#39;mod_time&#39;: self._parse_dos_datetime(mod_date, mod_time) }) return entries def _parse_dos_datetime(self, date, time): """解析DOS日期时间格式""" try: # 解析日期: 7位年(从1980), 4位月, 5位日 year = ((date & 0xFE00) >> 9) + 1980 month = (date & 0x01E0) >> 5 day = date & 0x001F # 解析时间: 5位时, 6位分, 5位秒(以2秒为单位) hour = (time & 0xF800) >> 11 minute = (time & 0x07E0) >> 5 second = (time & 0x001F) * 2 return f"{year}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}" except: return "未知时间" def read_file_content(self, start_cluster, size): """读取文件内容""" content = b&#39;&#39; chain = self.get_cluster_chain(start_cluster) if not chain: return content bytes_remaining = size for cluster in chain: if bytes_remaining <= 0: break # 计算簇位置 sector_offset = self.data_start + (cluster - 2) * self.cluster_size if sector_offset < 0 or sector_offset > self.get_disk_size(): break self.fd.seek(sector_offset) # 读取簇内容 bytes_to_read = min(bytes_remaining, self.cluster_size) try: chunk = self.fd.read(bytes_to_read) if not chunk: break content += chunk bytes_remaining -= len(chunk) except Exception as e: messagebox.showerror("读取错误", f"读取簇 {cluster} 失败: {str(e)}") break return content def get_disk_size(self): """获取磁盘大小""" try: self.fd.seek(0, 2) # 移动到文件末尾 return self.fd.tell() except Exception as e: messagebox.showerror("错误", f"获取磁盘大小失败: {str(e)}") return 0 def create_file(self, parent_cluster, filename, content): """在指定目录创建文件""" if self.readonly: raise PermissionError("只读模式不允许写操作") # 1. 分配簇链 clusters_needed = math.ceil(len(content) / self.cluster_size) if clusters_needed > len(self.free_clusters): raise IOError("磁盘空间不足") allocated_clusters = self.free_clusters[:clusters_needed] self.free_clusters = self.free_clusters[clusters_needed:] # 2. 更新FAT表 for i in range(len(allocated_clusters)): cluster = allocated_clusters[i] if i == len(allocated_clusters) - 1: # 最后一个簇标记为EOF self.fat[cluster] = 0x0FFFFFFF else: # 指向下一个簇 self.fat[cluster] = allocated_clusters[i+1] # 3. 写入FAT表 self._write_fat_table() # 4. 写入文件内容 for i, cluster in enumerate(allocated_clusters): offset = self.data_start + (cluster - 2) * self.cluster_size self.fd.seek(offset) # 写入当前簇的数据 start = i * self.cluster_size end = min((i+1) * self.cluster_size, len(content)) self.fd.write(content[start:end]) # 5. 在父目录创建目录项 self._create_directory_entry(parent_cluster, { &#39;name&#39;: filename, &#39;is_dir&#39;: False, &#39;start_cluster&#39;: allocated_clusters[0], &#39;size&#39;: len(content) }) return allocated_clusters[0] def create_directory(self, parent_cluster, dirname): """在指定目录创建子目录""" if self.readonly: raise PermissionError("只读模式不允许写操作") # 1. 分配一个簇给新目录 if not self.free_clusters: raise IOError("磁盘空间不足") cluster = self.free_clusters[0] self.free_clusters = self.free_clusters[1:] # 2. 更新FAT表 (目录结束) self.fat[cluster] = 0x0FFFFFFF self._write_fat_table() # 3. 初始化目录内容 (创建 . 和 .. 条目) self._initialize_directory(cluster, parent_cluster) # 4. 在父目录创建目录项 self._create_directory_entry(parent_cluster, { &#39;name&#39;: dirname, &#39;is_dir&#39;: True, &#39;start_cluster&#39;: cluster, &#39;size&#39;: 0 }) return cluster def _initialize_directory(self, cluster, parent_cluster): """初始化新目录内容""" # 计算簇位置 offset = self.data_start + (cluster - 2) * self.cluster_size # 创建 . 条目 dot_entry = self._create_dir_entry(".", cluster, True) # 创建 .. 条目 dotdot_entry = self._create_dir_entry("..", parent_cluster, True) # 写入目录内容 self.fd.seek(offset) self.fd.write(dot_entry) self.fd.write(dotdot_entry) # 填充剩余空间为0 remaining = self.cluster_size - len(dot_entry) - len(dotdot_entry) self.fd.write(b&#39;\x00&#39; * remaining) def _create_dir_entry(self, name, cluster, is_dir): """创建目录项字节数据""" # 短文件名格式 (8.3) if len(name) > 8 and &#39;.&#39; not in name: base, ext = name[:8], "" else: parts = name.split(&#39;.&#39;) base = parts[0].upper()[:8] ext = parts[1].upper()[:3] if len(parts) > 1 else "" # 填充空格 base = base.ljust(8, &#39; &#39;) ext = ext.ljust(3, &#39; &#39;) # 创建32字节条目 entry = bytearray(32) # 文件名 (8字节) entry[0:8] = base.encode(&#39;latin-1&#39;) # 扩展名 (3字节) entry[8:11] = ext.encode(&#39;latin-1&#39;) # 属性 (目录) entry[11] = 0x10 if is_dir else 0x20 # 目录或存档 # 创建时间和日期 (当前时间) now = datetime.datetime.now() create_time = self._to_dos_time(now) create_date = self._to_dos_date(now) entry[14:16] = struct.pack(&#39;<H&#39;, create_time) entry[16:18] = struct.pack(&#39;<H&#39;, create_date) # 修改时间和日期 mod_time = create_time mod_date = create_date entry[22:24] = struct.pack(&#39;<H&#39;, mod_time) entry[24:26] = struct.pack(&#39;<H&#39;, mod_date) # 起始簇号 entry[20:22] = struct.pack(&#39;<H&#39;, (cluster >> 16) & 0xFFFF) # 高16位 entry[26:28] = struct.pack(&#39;<H&#39;, cluster & 0xFFFF) # 低16位 # 文件大小 (目录为0) entry[28:32] = struct.pack(&#39;<I&#39;, 0) return entry def _to_dos_time(self, dt): """将datetime转换为DOS时间格式""" return ((dt.hour << 11) | (dt.minute << 5) | (dt.second // 2)) def _to_dos_date(self, dt): """将datetime转换为DOS日期格式""" return (((dt.year - 1980) << 9) | (dt.month << 5) | dt.day) def _create_directory_entry(self, parent_cluster, entry_info): """在父目录中添加新的目录项""" # 获取父目录的所有簇 clusters = self.get_cluster_chain(parent_cluster) if not clusters: raise IOError("父目录无效") # 查找空闲目录槽 for cluster in clusters: offset = self.data_start + (cluster - 2) * self.cluster_size self.fd.seek(offset) data = self.fd.read(self.cluster_size) for i in range(0, len(data), 32): pos = offset + i entry = data[i:i+32] # 找到空闲或已删除的条目 if len(entry) < 32 or entry[0] in [0x00, 0xE5]: # 创建新条目 new_entry = self._create_dir_entry( entry_info[&#39;name&#39;], entry_info[&#39;start_cluster&#39;], entry_info[&#39;is_dir&#39;] ) # 设置文件大小 if not entry_info[&#39;is_dir&#39;]: new_entry[28:32] = struct.pack(&#39;<I&#39;, entry_info[&#39;size&#39;]) # 写入新条目 self.fd.seek(pos) self.fd.write(new_entry) return # 如果没有找到空闲槽,分配新簇 if not self.free_clusters: raise IOError("磁盘空间不足") new_cluster = self.free_clusters[0] self.free_clusters = self.free_clusters[1:] # 更新FAT表 self.fat[clusters[-1]] = new_cluster # 当前最后一个簇指向新簇 self.fat[new_cluster] = 0x0FFFFFFF # 新簇标记为EOF self._write_fat_table() # 初始化新簇 self.fd.seek(self.data_start + (new_cluster - 2) * self.cluster_size) self.fd.write(b&#39;\x00&#39; * self.cluster_size) # 写入新条目到新簇的第一个位置 new_entry = self._create_dir_entry( entry_info[&#39;name&#39;], entry_info[&#39;start_cluster&#39;], entry_info[&#39;is_dir&#39;] ) if not entry_info[&#39;is_dir&#39;]: new_entry[28:32] = struct.pack(&#39;<I&#39;, entry_info[&#39;size&#39;]) self.fd.seek(self.data_start + (new_cluster - 2) * self.cluster_size) self.fd.write(new_entry) def _write_fat_table(self): """将FAT表写回磁盘""" # 更新所有FAT副本 for fat_copy in range(self.num_fats): offset = self.fat_start + fat_copy * self.sectors_per_fat * self.sector_size self.fd.seek(offset) # 构建FAT表数据 fat_data = bytearray() for entry in self.fat: # 确保使用32位格式 fat_data += struct.pack(&#39;<I&#39;, entry) # 写入FAT表 self.fd.write(fat_data) def close(self): """安全关闭文件句柄""" if hasattr(self, &#39;fd&#39;) and self.fd: self.fd.close() class FAT32Explorer: def __init__(self, root, device_path, readonly=True): self.root = root self.root.title(f"FAT32 文件系统分析工具 - {device_path}") self.root.geometry("1400x900") self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.device_path = device_path self.readonly = readonly self.current_cluster = None self.current_path = "/" self.selected_file = None self.file_content_cache = None self.canvas = None self.cluster_canvas = None # 创建GUI布局 self._create_widgets() # 在后台加载文件系统 self.loading = True self.status = tk.StringVar() self.status.set("正在加载文件系统...") self.root.after(100, self._load_filesystem) def _create_widgets(self): # 创建主框架 main_frame = tk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 顶部工具栏 toolbar = tk.Frame(main_frame) toolbar.pack(fill=tk.X, pady=(0, 10)) mode_text = "只读模式" if self.readonly else "读写模式" mode_color = "green" if self.readonly else "red" tk.Label(toolbar, text=f"模式: {mode_text}", fg=mode_color, font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=10) # 路径导航 self.path_var = tk.StringVar(value="路径: /") tk.Label(toolbar, textvariable=self.path_var, font=("Arial", 10)).pack(side=tk.LEFT, padx=10) # 操作按钮 btn_frame = tk.Frame(toolbar) btn_frame.pack(side=tk.RIGHT) tk.Button(btn_frame, text="刷新", command=self.refresh).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="物理布局", command=self.show_physical_layout).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="FAT表信息", command=self.show_fat_info).pack(side=tk.LEFT, padx=2) if not self.readonly: tk.Button(btn_frame, text="新建文件", command=self.create_file_dialog).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="新建目录", command=self.create_directory_dialog).pack(side=tk.LEFT, padx=2) tk.Button(btn_frame, text="写入测试文件", command=self.write_test_file).pack(side=tk.LEFT, padx=2) # 主分割窗口 self.paned_window = tk.PanedWindow(main_frame, orient=tk.HORIZONTAL) self.paned_window.pack(fill=tk.BOTH, expand=True) # 左侧面板 (目录树) self.left_frame = tk.LabelFrame(self.paned_window, text="目录结构") self.paned_window.add(self.left_frame, width=300) # 目录树 self.tree = ttk.Treeview(self.left_frame, show=&#39;tree&#39;, columns=("size")) self.tree.heading("#0", text="名称") self.tree.heading("size", text="大小") self.tree.column("size", width=80, anchor=&#39;e&#39;) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar = ttk.Scrollbar(self.left_frame, orient=tk.VERTICAL, command=self.tree.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.tree.configure(yscrollcommand=scrollbar.set) self.tree.bind(&#39;<<TreeviewSelect>>&#39;, self.on_tree_select) # 右侧面板 self.right_frame = tk.PanedWindow(self.paned_window, orient=tk.VERTICAL) self.paned_window.add(self.right_frame) # 上部: 文件列表 file_frame = tk.LabelFrame(self.right_frame, text="当前目录内容") file_frame.pack(fill=tk.BOTH, expand=True) # 文件列表表头 columns = ("name", "size", "type", "cluster", "modified", "attributes") self.file_tree = ttk.Treeview(file_frame, columns=columns, show="headings") # 设置列 self.file_tree.heading("name", text="名称") self.file_tree.heading("size", text="大小") self.file_tree.heading("type", text="类型") self.file_tree.heading("cluster", text="起始簇") self.file_tree.heading("modified", text="修改时间") self.file_tree.heading("attributes", text="属性") self.file_tree.column("name", width=200) self.file_tree.column("size", width=80, anchor=&#39;e&#39;) self.file_tree.column("type", width=80) self.file_tree.column("cluster", width=80, anchor=&#39;e&#39;) self.file_tree.column("modified", width=150) self.file_tree.column("attributes", width=80) self.file_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.file_tree.bind(&#39;<<TreeviewSelect>>&#39;, self.on_file_select) self.file_tree.bind(&#39;<Double-1>&#39;, self.on_file_double_click) # 文件列表滚动条 file_scrollbar = ttk.Scrollbar(file_frame, orient=tk.VERTICAL, command=self.file_tree.yview) file_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.file_tree.configure(yscrollcommand=file_scrollbar.set) # 添加右键菜单 self.file_menu = tk.Menu(self.root, tearoff=0) self.file_menu.add_command(label="查看内容", command=self.show_file_content) self.file_menu.add_command(label="查看簇链", command=self.show_cluster_chain) self.file_menu.add_command(label="计算哈希", command=self.calculate_hash) self.file_tree.bind("<Button-3>", self.show_context_menu) # 下部: 簇状态可视化 self.cluster_frame = tk.LabelFrame(self.right_frame, text="簇分配图") self.cluster_frame.pack(fill=tk.BOTH, expand=True) # 簇链可视化框架 self.cluster_canvas_frame = tk.Frame(self.cluster_frame) self.cluster_canvas_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 底部: 状态栏 self.status = tk.StringVar() self.status.set("就绪" + (" - 只读模式" if self.readonly else " - 读写模式")) status_bar = tk.Label(self.root, textvariable=self.status, bd=1, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 添加文件内容查看器 (弹出窗口) self.content_window = None self.cluster_window = None def _load_filesystem(self): """安全加载文件系统""" try: self.parser = FAT32Parser(self.device_path, self.readonly) self.root_cluster = self.parser.root_cluster # 加载根目录 self._show_root_directory() self.status.set(f"就绪 | 总簇数: {len(self.parser.fat)} | 空闲簇: {len(self.parser.free_clusters)}") self.loading = False # 更新簇分配图 self._update_cluster_map() except Exception as e: self.status.set(f"错误: {str(e)}") messagebox.showerror("初始化错误", f"加载文件系统失败: {str(e)}") self.root.after(100, self.root.destroy) def _show_root_directory(self): """显示根目录""" self.tree.delete(*self.tree.get_children()) self.file_tree.delete(*self.file_tree.get_children()) self.current_path = "/" self.path_var.set(f"路径: {self.current_path}") # 添加根节点 root_id = self.tree.insert(&#39;&#39;, &#39;end&#39;, text="根目录", values=[""], open=True) self.current_cluster = self.root_cluster # 加载根目录内容 self._load_directory(root_id, self.root_cluster) def _load_directory(self, parent_id, cluster): """加载目录内容到树视图和列表""" try: entries = self.parser.read_directory(cluster) self.current_entries = entries # 清空文件列表 self.file_tree.delete(*self.file_tree.get_children()) # 添加到文件列表 for entry in entries: # 跳过特殊目录项 &#39;.&#39; 和 &#39;..&#39; if entry[&#39;name&#39;] in [&#39;.&#39;, &#39;..&#39;]: continue item_type = "目录" if entry[&#39;is_dir&#39;] else "文件" size = self.format_size(entry[&#39;size&#39;]) if not entry[&#39;is_dir&#39;] else "" # 构建属性字符串 attributes = [] if entry[&#39;is_hidden&#39;]: attributes.append("隐藏") if entry[&#39;is_system&#39;]: attributes.append("系统") if entry[&#39;is_archive&#39;]: attributes.append("存档") attr_str = ", ".join(attributes) self.file_tree.insert(&#39;&#39;, &#39;end&#39;, values=( entry[&#39;name&#39;], size, item_type, entry[&#39;start_cluster&#39;], entry[&#39;mod_time&#39;], attr_str )) # 添加到树视图 for entry in entries: # 跳过特殊目录项 &#39;.&#39; 和 &#39;..&#39; if entry[&#39;name&#39;] in [&#39;.&#39;, &#39;..&#39;]: continue if entry[&#39;is_dir&#39;]: size = self.format_size(entry[&#39;size&#39;]) node_id = self.tree.insert(parent_id, &#39;end&#39;, text=entry[&#39;name&#39;], values=[entry[&#39;start_cluster&#39;]], tags=(&#39;dir&#39;,)) # 添加一个虚拟节点以便展开 self.tree.insert(node_id, &#39;end&#39;, text="加载中...", tags=(&#39;dummy&#39;,)) # 配置标签样式 self.tree.tag_configure(&#39;dir&#39;, foreground=&#39;blue&#39;) self.tree.tag_configure(&#39;dummy&#39;, foreground=&#39;gray&#39;) self.status.set(f"显示目录: 簇 {cluster} | 条目: {len(entries)}") except Exception as e: self.status.set(f"错误: {str(e)}") messagebox.showerror("目录错误", f"读取目录失败: {str(e)}") def _update_cluster_map(self): """更新簇状态可视化""" if not hasattr(self, &#39;parser&#39;): return # 清除现有图表 if self.canvas: self.canvas.get_tk_widget().destroy() fig, ax = plt.subplots(figsize=(10, 3)) # 简化的簇状态展示 max_clusters = min(1000, len(self.parser.fat)) # 创建状态数组,每个元素对应一个簇的状态 cluster_status = [] # 收集前max_clusters个簇的状态 for cluster_idx in range(max_clusters): # 标记特殊簇 if cluster_idx == 0: cluster_status.append(4) # 特殊簇 elif cluster_idx == 1: cluster_status.append(4) # 特殊簇 elif self.parser.fat[cluster_idx] == 0: cluster_status.append(0) # 空闲 elif self.parser.fat[cluster_idx] >= 0x0FFFFFF8: cluster_status.append(1) # 文件结束 elif self.parser.fat[cluster_idx] == 0x0FFFFFF7: cluster_status.append(3) # 坏簇 else: cluster_status.append(2) # 使用中 # 修复:将一维列表转换为二维数组用于热力图 # 计算合适的行数和列数 num_cols = 50 # 每行50个簇 num_rows = (len(cluster_status) + num_cols - 1) // num_cols # 创建二维数组 heatmap_data = [] for i in range(num_rows): start_idx = i * num_cols end_idx = min((i + 1) * num_cols, len(cluster_status)) row = cluster_status[start_idx:end_idx] # 如果行不满,填充0 if len(row) < num_cols: row += [0] * (num_cols - len(row)) heatmap_data.append(row) # 使用不同颜色 cmap = plt.cm.colors.ListedColormap([ &#39;green&#39;, # 0: 空闲 &#39;red&#39;, # 1: 文件结束 &#39;blue&#39;, # 2: 使用中 &#39;black&#39;, # 3: 坏簇 &#39;gray&#39;, # 4: 特殊簇 ]) # 绘制热力图 img = ax.imshow(heatmap_data, cmap=cmap, aspect=&#39;auto&#39;) # 添加颜色条 cbar = fig.colorbar(img, ax=ax, ticks=[0, 1, 2, 3, 4]) cbar.ax.set_yticklabels([&#39;空闲&#39;, &#39;结束簇&#39;, &#39;使用中&#39;, &#39;坏簇&#39;, &#39;特殊簇&#39;]) ax.set_title(f&#39;簇分配图 (前{max_clusters}个簇)&#39;) ax.set_xlabel(&#39;簇号 (每行50个)&#39;) ax.set_ylabel(&#39;簇组&#39;) # 添加网格线 ax.grid(which=&#39;major&#39;, color=&#39;gray&#39;, linestyle=&#39;-&#39;, linewidth=0.5) ax.set_xticks(range(0, num_cols, 5)) ax.set_yticks(range(0, num_rows, 5)) # 嵌入到Canvas canvas = FigureCanvasTkAgg(fig, master=self.cluster_canvas_frame) canvas.draw() canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.canvas = canvas def show_context_menu(self, event): """显示右键菜单""" item = self.file_tree.identify_row(event.y) if item: self.file_tree.selection_set(item) self.file_menu.post(event.x_root, event.y_root) def on_tree_select(self, event): """处理树节点选择事件""" if self.loading: return selected = self.tree.selection() if not selected: return item = self.tree.item(selected[0]) if &#39;values&#39; in item and item[&#39;values&#39;]: # 如果是虚拟节点,跳过 if &#39;dummy&#39; in self.tree.item(selected[0], "tags"): return # 获取簇号(目录节点才有) if &#39;dir&#39; in self.tree.item(selected[0], "tags"): cluster = self.tree.item(selected[0])[&#39;values&#39;][0] if isinstance(cluster, int) or cluster.isdigit(): self.current_cluster = int(cluster) else: # 根目录 self.current_cluster = self.parser.root_cluster # 构建当前路径 path = [] current_item = selected[0] while current_item: item_text = self.tree.item(current_item)[&#39;text&#39;] if item_text != "根目录": path.insert(0, item_text) current_item = self.tree.parent(current_item) self.current_path = "/" + "/".join(path) self.path_var.set(f"路径: {self.current_path}") # 如果节点有子节点但只有一个"加载中"节点,则加载实际内容 children = self.tree.get_children(selected[0]) if children and self.tree.item(children[0])[&#39;text&#39;] == "加载中...": self.tree.delete(children[0]) self._load_directory(selected[0], self.current_cluster) else: self._load_directory(selected[0], self.current_cluster) def on_file_select(self, event): """处理文件列表选择事件""" if self.loading: return selected = self.file_tree.selection() if not selected: return item = self.file_tree.item(selected[0]) values = item[&#39;values&#39;] if values: # 查找对应的条目 for entry in self.current_entries: if entry[&#39;name&#39;] == values[0]: self.selected_file = entry break def on_file_double_click(self, event): """双击文件事件""" if self.selected_file and not self.selected_file[&#39;is_dir&#39;]: self.show_file_content() def show_file_content(self): """显示文件内容""" if not self.selected_file or self.selected_file[&#39;is_dir&#39;]: return # 创建弹出窗口 if self.content_window and self.content_window.winfo_exists(): self.content_window.destroy() self.content_window = tk.Toplevel(self.root) self.content_window.title(f"文件内容: {self.selected_file[&#39;name&#39;]}") self.content_window.geometry("800x600") # 添加文本编辑器 text_frame = tk.Frame(self.content_window) text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 添加文本区域 self.content_text = scrolledtext.ScrolledText(text_frame, wrap=tk.WORD) self.content_text.pack(fill=tk.BOTH, expand=True) # 添加按钮 btn_frame = tk.Frame(self.content_window) btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) tk.Button(btn_frame, text="加载内容", command=self.load_file_content).pack(side=tk.LEFT, padx=5) if not self.readonly: tk.Button(btn_frame, text="保存修改", command=self.save_file_content).pack(side=tk.LEFT, padx=5) tk.Button(btn_frame, text="关闭", command=self.content_window.destroy).pack(side=tk.RIGHT, padx=5) def load_file_content(self): """加载文件内容到编辑器""" if not self.selected_file: return try: content = self.parser.read_file_content( self.selected_file[&#39;start_cluster&#39;], self.selected_file[&#39;size&#39;] ) # 尝试解码为文本 try: decoded = content.decode(&#39;utf-8&#39;, errors=&#39;replace&#39;) self.content_text.delete(1.0, tk.END) self.content_text.insert(tk.END, decoded) self.file_content_cache = content self.status.set(f"已加载文件: {self.selected_file[&#39;name&#39;]}") except UnicodeDecodeError: # 如果是二进制文件,显示十六进制预览 hex_preview = &#39; &#39;.join(f&#39;{b:02x}&#39; for b in content[:128]) if len(content) > 128: hex_preview += " ..." self.content_text.delete(1.0, tk.END) self.content_text.insert(tk.END, f"二进制文件 (十六进制预览):\n{hex_preview}") self.file_content_cache = content self.status.set(f"已加载二进制文件: {self.selected_file[&#39;name&#39;]}") except Exception as e: self.status.set(f"错误: {str(e)}") messagebox.showerror("读取错误", f"读取文件内容失败: {str(e)}") def save_file_content(self): """保存修改后的文件内容""" if self.readonly or not self.selected_file or not self.file_content_cache: return # 获取新内容 new_content = self.content_text.get(1.0, tk.END).encode(&#39;utf-8&#39;) # 检查内容是否变化 if new_content == self.file_content_cache: messagebox.showinfo("保存", "内容未更改") return # 确认保存 confirm = messagebox.askyesno("确认保存", f"确定要保存对文件 &#39;{self.selected_file[&#39;name&#39;]}&#39; 的修改吗?\n" "此操作将直接写入U盘!") if not confirm: return try: # 创建新文件 (如果大小变化) if len(new_content) != len(self.file_content_cache): # 创建新文件 new_cluster = self.parser.create_file( self.current_cluster, self.selected_file[&#39;name&#39;], new_content ) # 删除旧文件 (标记为删除) # 在实际应用中应该实现文件删除功能 messagebox.showinfo("保存成功", "文件大小已改变,已创建新文件副本") else: # 直接覆盖内容 chain = self.parser.get_cluster_chain(self.selected_file[&#39;start_cluster&#39;]) bytes_remaining = len(new_content) for i, cluster in enumerate(chain): if bytes_remaining <= 0: break offset = self.parser.data_start + (cluster - 2) * self.parser.cluster_size self.parser.fd.seek(offset) # 写入当前簇的数据 start = i * self.parser.cluster_size end = min((i+1) * self.parser.cluster_size, len(new_content)) self.parser.fd.write(new_content[start:end]) bytes_remaining -= (end - start) self.status.set(f"文件已保存: {self.selected_file[&#39;name&#39;]}") messagebox.showinfo("保存成功", "文件内容已更新") # 刷新目录 self.refresh() 修正代码
06-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值