TreeView使用总汇聚

本文介绍了如何在ASP.NET中将TreeView和Menu控件与关系数据库进行绑定,以实现动态数据加载。在使用AJAX的UpdatePanel中,对TreeView的动态绑定提供了良好的用户体验。然而,Menu控件在UpdatePanel内存在显示问题,因此选择在Page_Load事件中一次性绑定,以避免影响用户体验。通过递归添加节点和DataView的RowFilter属性筛选下级节点,实现了数据库绑定的菜单结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

总结了一下用MS的TREE控件的心得,大家一起来分享,当然,也不乏有其他网友的一些好的见解被录入其中,另有遗漏处请大家不吝赐教!

相信只要你仔细阅读了这篇小菜文,并参考一下提供的例程,一定能够掌握TREE的基本用法!

1.工欲善其事,必先利其器。首先保证装好你的组件,这个包安装非常方便,只要运行一下即可在VS.NET的工具栏中找到并使用了:
http://218.56.11.178:8018/FileDown.aspx?FID=4
也可以关注一下官方站是否有新版发布:
http://msdn.microsoft.com/downloads/samples/internet/default.asp?url=/Downloads/samples/Internet/ASP_DOT_NET_ServerControls/WebControls/default.asp

2.常用的几个属性和方法
~Index 获取树节点在树节点集合中的位置。
~Nodes 获取分配给树视图控件的树节点集合。
~Parent 获取或设置控件的父容器。
~SelectedNode 获取或设置当前在树视图控件中选定的树节点。
~ExpandAll 展开所有树节点。
~Checked 获取或设置一个值,用以指示树节点是否处于选中状态。
~Text 获取或设置在树节点标签中显示的文本。
~Expand 展开树节点。
~Clear 清空树
~Remove 从树视图控件中移除当前树节点。
以上由其他网友总结,补充:
~Height 控件的高度
~Width 控件的宽度
~BackColor 背景颜色
~BorderColor 边框颜色
~BorderStyle 边框样式
~BorderWidth 边框宽度
~CssClass 应用于该控件的CSS类名
~ExpandedImageUrl 展开时显示的节点图标
~ImageUrl 未选择或展开是显示的节点图标
~SelectedImageUrl 选中状态下显示的节点图标
~Indent 缩进距离,只有在ShowLines设为TRUE时才生效。
~ShowLines 是否显示层级连接线
~ShowPlus 是否显示+/-符号按钮
~ShowToolTip 在有父节点上显示工具提示(+/-号的使用展开/关闭)。
~AccseeKey 控件使用的键盘快捷键
~AutoSelect 为TRUE时,当用键盘移动节点时,自动选择新节点
~AutoPostBack 当改变状态时,自动回存
~Enabled 控制控件的启用状态
~EnableViewState 控件是否自动保存其状态以用于往返行程
~ExpandLevel 初始化控件是展开节点的层数
~SelectExpands 当选中一个接点时,是否自动展开该节点
~TabIndex TAB键次序
~Visible 控件是否可见

3.实用技巧:
1)怎样点击文字(不是+/-号)即可展开(收缩)子节点
将TREE的ShowToolTip 属性设为false即可。

2)当鼠标指到某父接点时,如何不显示显示“节点名:user +/- to expand/collapse”
将TREE的SelectExpands 属性设为TRUE即可。

3)不显示树型的问题
首先:控件包没装好,使用上边介绍的控件包装一下试试。
其次:TreeView要求客户端浏览器版本为IE5.5及以上,最好要求客户端升级为IE6.0

4)关于闪烁
将AutoPostBack属性设置为真,SelectedIndexChange才能被执行。不过这样的话刷新的很厉害。不要刷新的话,将AutoPostBack属性设置为FALSE.

5)如何实现用键盘上下左右键移动焦点时,自动选择新节点并执行新节点
将TREE的AutoSelect 属性设为TRUE即可。

6)不想显示+/-符号按钮怎么做?
将TREE的ShowPlus 属性设为False即可。

7)不想显示层级连接线怎么做?
将TREE的ShowLines 属性设为False即可。

8)如何设置节点旁的图标
~ExpandedImageUrl、ImageUrl、SelectedImageUrl这3个属性是控制图标的,选择自己喜欢的就OK了。

4.看看这个例子,对你一定有启发,是一棵无级树,不过在实际使用中可能会有些麻烦:
http://218.56.11.178:8018/FileDown.aspx?FID=246

5.这个论坛也使用了树(asp.net+C#+MSSQL 2000),并且代码开放,,如果觉得有参考价值,不妨装起来看看,其中还包含了其他一些常用的asp.net编程技巧,演示了TREE控件如何和数据库结合,动态显示库中的数据,这棵树只有两层,不过比较实用:
http://218.56.11.178:8018/FileDown.aspx?FID=212
其中:tree.aspx、tree.aspx.cs是TREE控件使用的主要部分。

论坛的实际应用演示地址,在这里:
http://expert.kaer.cn/








TreeView使用集锦   hgknight(原作)

原来在论坛里发过专题帖子,只是由于帖子丢失原因,有些问题找不到了,同时这次也补充了一些,发到文档区以方便查询吧
原贴
http://expert.youkuaiyun.com/Expert/topic/1525/1525202.xml

1.下载地址
http://msdn.microsoft.com/downloads/samples/internet/ASP_DOT_NET_ServerControls/WebControls/default.asp
下载后是后缀为bat的版本
(1)bulid.将bulid.bat的路径指向csc.exe所在路径,生成Microsoft.Web.UI.WebControls.dll。
(2)在wwwroot下创建空目录webctrl_client/1_0。
(3)将build/Runtime下的文件拷至webctrl_client/1_0下。
(4)选择工具箱的自定义工具箱,添加Microsoft.Web.UI.WebControls.dll。
有些麻烦
但如果你能找到后缀是msi的自动安装版本,直接下一步就行(我一直用这个版本,hoho)
安装后,通过“自定义工具箱”->“.net框架组件”把TreeView添加到工具箱里

2.运行时无法显示
一般是TreeView的版本问题,最好下载英文版自动安装版本重新安装,安装前应该先到添加删除程序里卸掉原版本

3.显示格式出错(非树状显示)
TreeView要求客户端浏览器版本为IE5.5及以上,最好要求客户端升级为IE6.0

4.框架里使用TreeView
设置NavigateUrl、Target属性,可更新另外的Frame

5.找不到TreeNode类
使用TreeView,最好添加namespace:using Microsoft.Web.UI.WebControls;

6.遍历TreeView节点(递归算法)
private void Page_Load(object sender, System.EventArgs e)
{
GetAllNodeText(TreeView1.Nodes);
}
void GetAllNodeText(TreeNodeCollection tnc)
{
foreach(TreeNode node in tnc)
{
if(node.Nodes.Count!=0)
  GetAllNodeText(node.Nodes);
Response.Write(node.Text + " ");
}
}

7.得到node结点的父节点
TreeNode pnode;
if(node.Parent is TreeNode)
pnode=(TreeNode)node.Parent;
else
//node is root node

8.修改TreeView样式(示例)
<iewc:TreeView id="TreeView1" runat="server" HoverStyle="color:blue;background:#00ffCC;" DefaultStyle="background:red;color:yellow;" SelectedStyle="color:red;background:#00ff00;">
用代码:
TreeView1.DefaultStyle["font-size"] = "20pt";

9.展开时不提交,改变选择节点时才提交
将autopostback设置成false;
在body里添加 <body onload="initTree()">
然后在PageLoad里写:
string strTreeName = "TreeView1";
string strRef = Page.GetPostBackEventReference(TreeView1);
string strScript = "<script language=/"JavaScript/"> /n" + "<!-- /n" + "         function initTree() { /n" +"                 " + strTreeName + ".onSelectedIndexChange = function() { /n" +   "if (event.oldTreeNodeIndex !=  
event.newTreeNodeIndex) /n" + "this.queueEvent('onselectedindexchange', event.oldTreeNodeIndex + ',' + event.newTreeNodeIndex); /n" +   "window.setTimeout('" + strRef.Replace("'","//'")   + "', 0, 'JavaScript'); /n" +   "                 } /n" +     "         } /n" +   "// --> /n" + "</script>";
Page.RegisterClientScriptBlock("InitTree",strScript );

这样就只有你点击的节点更改的时候才提交!

10.TreeView结合XML
把XML文件设置为如下格式,然后直接设置TreeNodeSrc为该XML文件就行
<?xml version="1.0" encoding="GB2312"?>
<TREENODES>
<TREENODE TEXT="node0" EXPANDED="true">
<TREENODE TEXT="node1"/>
<TREENODE TEXT="node2"/>
</TREENODE>
<TREENODE TEXT="node3" NavigateURL="3.aspx"/>
</TREENODES>
或者用代码
TreeView1.TreeNodesrc="a.xml";
TreeView1.DataBind();



客户端控制TreeView
http://expert.youkuaiyun.com/Expert/topic/1382/1382892.xml

1.设置所选节点,如选中第二个节点
function SetSelNode()
{
TreeView1.selectedNodeIndex="1";
}

2.得到所选节点的Text,ID或NodeData
function GetAttribute()
{
alert(TreeView1.getTreeNode(TreeView1.selectedNodeIndex).getAttribute("Text"));
}
替换Text为ID或NodeData,可分别得到所选节点的ID或NodeData

3.修改节点属性,如修改第一个节点的Text
function ModifyNode()
{
var node=TreeView1.getTreeNode("0");
node.setAttribute("Text","hgknight");
}

4.得到点击节点
function TreeView1.onclick()
{
alert(TreeView1.getTreeNode(TreeView1.clickedNodeIndex).getAttribute("Text"));
}

5.添加节点
function AddNode()
{
var node=TreeView1.createTreeNode();
node.setAttribute("Text","hgknight");
TreeView1.add(node);  
}

6.js遍历所有节点
var AllRootNode=new Array();
AllRootNode=TreeView1.getChildren();
AlertNode(AllRootNode);  

function AlertNode(NodeArray)
{
if(parseInt(NodeArray.length)==0)
  return;
else
{
  for(i=0;i<NodeArray.length;i++)
  {
  var cNode;
  cNode=NodeArray ;
  alert(cNode.getAttribute("Text"));
  if(parseInt(cNode.getChildren().length)!=0)
  AlertNode(cNode.getChildren());  
  }
}
}


ASP.net2.0 为用户提供了treeview和menu导航控件,使用十分方便,极大提高了用户开发项目的速度和效率。

尤其是treeview和menu绑定web.sitemap之类的XML文件,形成层次数据非常便捷。

但我在项目开发过程中,很多地方是需要绑定关系数据库,毕竟关系数据库是最常用、操作最方便的存储形式。

对于treebview与关系数据库绑定可以动态绑定,也就是在page_load事件中首先添加第一级节点,然后在treeview的SelectedNodeChanged事件中进行判断添加下一级节点。因为使用AJAX技术后,把treeview控件放在UpdatePanel控件之内,用户的页面体验效果相当不错。

然而对于menu控件,这样就有麻烦了,因为我发现menu在UpdatePanel控件中有问题:就是移动鼠标后原来的菜单项会遗留在屏幕上,直到单击新的菜单项(不知道是bug,还是我哪里出现问题)。也许有人觉得menu不要动态绑定,直接绑定XML做成静态的多好!但在做项目时需要不同的用户登陆后出现的菜单效果不同,而在web.sitemap中roles角色控制又不是十分便利,所以使用关系数据库直接对menu菜单项的enabled进行赋值,控制更加灵活。

因为有bug,只能把menu控件放在UpdatePanel控件之外,对menu的绑定就要在page_load事件中一次性绑定,如果使用menu控件的MenuItemClick事件绑定,那么用户受不了这样的体验!

实现的效果如下:



具体实现代码如下:

 


 1<%
现在我的功能都能实现,但是最近上了一台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='ip_mac_bind.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', encoding='utf-8' ) # ----------------------- 配置参数 ----------------------- DEVICES = { 'oa_core_switch': { 'device_type': 'hp_comware', 'host': '172.17.21.1', 'username': 'user', 'password': 'CD5', 'timeout': 30 }, 'aggregation_switch': { 'device_type': 'hp_comware', 'username': 'user', 'password': 'CD345', 'timeout': 30 }, 'access_switch': { 'device_type': 'hp_comware', 'username': 'python', 'password': 'C345', 'timeout': 30, 'global_delay_factor': 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'[^0-9a-fA-F]', '', 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 'global_delay_factor' not in device_info: device_info['global_delay_factor'] = 2 conn = ConnectHandler(**device_info) logging.info(f"成功连接 {device_info['host']}") # 确保禁用分页 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['host']}: {str(e)}") time.sleep(delay) raise ConnectionError(f"无法连接 {device_info['host']}") def get_device_credentials(ip): """根据IP地址确定设备类型""" if ip == DEVICES['oa_core_switch']['host']: return DEVICES['oa_core_switch'].copy() if ip in AGGREGATION_SWITCHES: # 汇聚交换机使用自己的凭据 creds = DEVICES['aggregation_switch'].copy() creds['host'] = ip return creds else: # 默认使用接入交换机凭据 creds = DEVICES['access_switch'].copy() creds['host'] = ip return creds def parse_switch_name(switch_name): """将交换机名称转换为IP地址""" # 支持格式: SW201.55 或 sw201.55 pattern = r'^[sS][wW](\d{1,3})\.(\d{1,3})$' 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'(BAGG\d+|GE\S+|XGE\S+|Ten-GigabitEthernet\S+)' for line in output.splitlines(): # 查找Learned状态的端口 if 'Learned' in line: # 优先查找聚合端口 if 'BAGG' in line: match = re.search(r'BAGG\d+', line) if match: found_port = match.group() logging.info(f"在{conn.host}上找到聚合端口: {found_port}") return found_port, "AGG" # 查找物理端口 elif 'GE' in line or 'XGE' in line or 'Ten-GigabitEthernet' 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 'GE' in line or 'XGE' in line or 'BAGG' in line or 'Ten-GigabitEthernet' in line: match = re.search(port_pattern, line) if match: found_port = match.group() if 'BAGG' 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 'Management address' in line and ':' in line: ip_match = re.search(r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b', line) if ip_match: return ip_match.group() return None def get_lldp_neighbor(conn, port): """获取LLDP邻居信息""" try: # 确保端口格式正确,移除多余空格和括号 port = re.sub(r'[()]', '', port).strip() # H3C端口格式转换 if port.startswith('XGE'): converted_port = port.replace("XGE", "Ten-GigabitEthernet") elif port.startswith('GE'): 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('BAGG'): 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 ['GE', 'XGE', 'Ten']): port_name = parts[0] # 移除可能的参考端口标识 (R) if '(' in port_name: port_name = port_name.split('(')[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 ['GE', 'XGE', 'Ten']): match = re.search(r'(GE\S+|XGE\S+|Ten-GigabitEthernet\S+)', line) if match: port_name = match.group() if '(' in port_name: port_name = port_name.split('(')[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['oa_core_switch']['host'] 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['global_delay_factor'] = 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('clam') # 配置颜色 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=[('active', '#3a76d8')]) style.configure("Accent.TButton", background="#28a745", foreground="white", font=("Arial", 10, "bold")) style.map("Accent.TButton", background=[('active', '#218838')]) 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=[('active', '#c0392b')]) # 创建主框架 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('waiting', foreground='#6c757d') self.queue_tree.tag_configure('processing', foreground='#0d6efd') self.queue_tree.tag_configure('manual', foreground='#e67e22') self.queue_tree.tag_configure('auto', foreground='#3498db') self.queue_tree.tag_configure('success', foreground='#198754') self.queue_tree.tag_configure('failed', foreground='#dc3545') # 添加滚动条 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,点'添加手动绑定任务'") self.log_message("自动绑定区域: 输入IP和MAC,点'添加到队列(自动发现)'") def log_message(self, message): """添加消息到日志区域""" self.log_area.configure(state="normal") self.log_area.insert(tk.END, f"{time.strftime('%H:%M:%S')} - {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=('auto', 'waiting') ) 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=('manual', 'waiting') ) 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=('manual', 'waiting') ) 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=('auto', 'waiting') ) 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=('processing',) ) 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=('processing',) ) 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 = 'success' # 自动任务需要更新交换机信息 if task_type == "auto": # 尝试从结果中提取设备IP (例如 "✅ 成功: 在 172.17.201.55 上绑定...") match = re.search(r'在\s*(\d+\.\d+\.\d+\.\d+)\s*上', 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 = 'failed' 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'(\S+)\s*\(', 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=('processing',) ) 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 = 'success' else: status = "❌ 失败" tag = 'failed' 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, messagebox, filedialog import json import os import math class OpticalSystem: def __init__(self): self.surfaces = [] # 存储光学表面对象 self.entrance_pupil_diameter = None # 入瞳直径 (mm) self.entrance_pupil_position = None # 入瞳位置 (相对第一个面顶点) (mm) self.object_infinite = True # 物在无穷远 self.field_angle = None # 半视场角 (度) self.object_distance = None # 物距 (有限远) (mm) self.object_height = None # 物高 (有限远) (mm) self.aperture_angle = None # 孔径角 (有限远) (度) self.light_type = "d" # 色光类型,默认d光 # 计算结果存储 self.results = { "focal_length": None, # 焦距 f' "ideal_image_distance": None, # 理想像距 l' "actual_image_position": None, # 实际像位置 "image_principal_plane": None, # 像方主面位置 lH' "exit_pupil_distance": None, # 出瞳距 lp' "ideal_image_height": None, # 理想像高 y0' "spherical_aberration": None, # 球差 "longitudinal_chromatic": None, # 位置色差 "tangential_field_curvature": None, # 子午场曲 xt' "sagittal_field_curvature": None, # 弧矢场曲 xs' "astigmatism": None, # 像散 Δxts' "actual_image_height": None, # 实际像高 "relative_distortion": None, # 相对畸变 "absolute_distortion": None, # 绝对畸变 "lateral_chromatic": None, # 倍率色差 "tangential_coma": None # 子午慧差 } class Surface: def __init__(self, r=float('inf'), d=0.0, nd=1.0, vd=0.0): self.r = r # 曲率半径 (mm) self.d = d # 厚度/间隔 (mm) self.nd = nd # d光折射率 self.vd = vd # 阿贝数 def to_dict(self): """将表面数据转换为字典""" return { "r": self.r, "d": self.d, "nd": self.nd, "vd": self.vd } @classmethod def from_dict(cls, data): """从字典创建表面对象""" return cls( r=data.get('r', float('inf')), d=data.get('d', 0.0), nd=data.get('nd', 1.0), vd=data.get('vd', 0.0) ) class calculate: def trace_paraxial_ray(surfaces, h0, u0, n0=1.0): """ 近轴光线追迹函数 :param surfaces: 光学表面列表 :param h0: 初始光线高度 :param u0: 初始光线角度(弧度) :param n0: 初始介质折射率 :return: 最后的光线高度和角度 """ h = h0 u = u0 n = n0 # 当前介质折射率 for i, surf in enumerate(surfaces): # 计算曲率(平面时曲率为0) c = 0.0 if math.isinf(surf.r) else 1.0 / surf.r # 折射公式:n'u' = nu + (n - n') * c * h n_prime = surf.nd # 折射后折射率 u_prime = (n * u + (n - n_prime) * c * h) / n_prime # 如果当前不是最后一个面,计算传播到下个面的高度 if i < len(surfaces) - 1: d = surf.d # 到下一个面的距离 h_next = h + d * u_prime else: h_next = h # 最后一个面后不需要传播 # 更新参数 h = h_next u = u_prime n = n_prime # 更新为当前面后的折射率 return h, u def calculate_focal_length(surfaces): """ 计算系统焦距 :param surfaces: 光学表面列表 :return: 焦距值(mm) """ # 追迹平行于光轴的光线(无穷远物) h0 = 1.0 # 任意高度,取1便于计算 u0 = 0.0 # 平行于光轴 h_final, u_final = calculate.trace_paraxial_ray(surfaces, h0, u0) # 焦距 f' = -h0 / u_final if abs(u_final) < 1e-10: # 防止除零错误 return float('inf') return -h0 / u_final def calculate_ideal_image_distance(surfaces, object_infinite, object_distance=None): """ 计算理想像距 :param surfaces: 光学表面列表 :param object_infinite: 物是否在无穷远 :param object_distance: 物距(有限远时) :return: 理想像距(mm) """ if object_infinite: # 无穷远物:理想像距等于焦距对应的像距 return calculate.calculate_focal_length(surfaces) else: # 有限远物:使用高斯公式计算理想像距 if object_distance is None: raise ValueError("有限远物需要提供物距") # 计算系统焦距 f_prime = calculate.calculate_focal_length(surfaces) # 高斯公式:1/f' = 1/l' - 1/l # 其中 l 为物距(负值),l' 为像距 l = -object_distance # 物距为负值(物在左侧) # 计算像距 l' if abs(f_prime) < 1e-10: return float('inf') l_prime = 1 / (1/f_prime + 1/l) return l_prime def calculate_principal_plane_position(surfaces): """ 计算像方主面位置 :param surfaces: 光学表面列表 :return: 主面位置(相对于最后一个面顶点,正值表示在右侧) """ # 计算焦距 f_prime = calculate.calculate_focal_length(surfaces) # 追迹平行于光轴的光线 h0 = 1.0 # 初始高度 u0 = 0.0 # 平行于光轴 h_final, u_final = calculate.trace_paraxial_ray(surfaces, h0, u0) # 主面位置 lH' = -h_final / u_final - f_prime if abs(u_final) < 1e-10: return float('inf') return -h_final / u_final - f_prime class MainApplication: def __init__(self, root): self.root = root self.root.title("光学系统计算程序") self.root.geometry("1000x850") # 创建光学系统对象 self.system = OpticalSystem() # 创建GUI组件 self.create_widgets() # 加载默认设置(如果有) self.load_default_settings() def create_widgets(self): # 主框架 main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 文件操作框架 file_frame = ttk.LabelFrame(main_frame, text="文件操作") file_frame.pack(fill=tk.X, padx=5, pady=5) # 文件路径输入 ttk.Label(file_frame, text="文件路径:").grid(row=0, column=0, padx=5, pady=5) self.file_path_entry = ttk.Entry(file_frame, width=50) self.file_path_entry.grid(row=0, column=1, padx=5, pady=5) # 文件操作按钮 browse_btn = ttk.Button(file_frame, text="浏览...", command=self.browse_file) browse_btn.grid(row=0, column=2, padx=5, pady=5) load_btn = ttk.Button(file_frame, text="加载系统参数", command=self.load_system) load_btn.grid(row=0, column=3, padx=5, pady=5) save_btn = ttk.Button(file_frame, text="保存系统参数", command=self.save_system) save_btn.grid(row=0, column=4, padx=5, pady=5) # 左侧面板 - 输入参数 left_frame = ttk.LabelFrame(main_frame, text="系统参数输入") left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) # 右侧面板 - 结果展示 right_frame = ttk.LabelFrame(main_frame, text="计算结果") right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5) # 创建左侧面板的子组件 self.create_input_panel(left_frame) # 创建右侧面板的子组件 self.create_result_panel(right_frame) # 计算按钮 calc_frame = ttk.Frame(main_frame) calc_frame.pack(fill=tk.X, padx=5, pady=10) calc_btn = ttk.Button(calc_frame, text="开始计算", command=self.calculate, width=15) calc_btn.pack(side=tk.LEFT, padx=10) save_result_btn = ttk.Button(calc_frame, text="保存计算结果", command=self.save_results, width=15) save_result_btn.pack(side=tk.LEFT, padx=10) clear_btn = ttk.Button(calc_frame, text="清除所有", command=self.clear_all, width=15) clear_btn.pack(side=tk.LEFT, padx=10) def browse_file(self): """浏览文件按钮处理函数""" file_path = filedialog.askopenfilename( title="选择文件", filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")] ) if file_path: self.file_path_entry.delete(0, tk.END) self.file_path_entry.insert(0, file_path) def create_input_panel(self, parent): # 入瞳参数 pupil_frame = ttk.LabelFrame(parent, text="入瞳参数") pupil_frame.pack(fill=tk.X, padx=5, pady=5) ttk.Label(pupil_frame, text="入瞳直径 (mm):").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.entrance_diameter_entry = ttk.Entry(pupil_frame, width=15) self.entrance_diameter_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Label(pupil_frame, text="入瞳位置 (mm):").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.entrance_position_entry = ttk.Entry(pupil_frame, width=15) self.entrance_position_entry.grid(row=0, column=3, padx=5, pady=5) # 色光类型选择 ttk.Label(pupil_frame, text="色光类型:").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.light_type_var = tk.StringVar(value="d") self.light_type_combo = ttk.Combobox(pupil_frame, textvariable=self.light_type_var, width=12, state="readonly") self.light_type_combo["values"] = ("d", "f", "c") self.light_type_combo.grid(row=1, column=1, padx=5, pady=5, sticky="w") # 物方参数 object_frame = ttk.LabelFrame(parent, text="物方参数") object_frame.pack(fill=tk.X, padx=5, pady=5) # 物距选择 self.object_var = tk.BooleanVar(value=True) # True: 无穷远, False: 有限远 ttk.Radiobutton(object_frame, text="物在无穷远", variable=self.object_var, value=True, command=self.toggle_object_input).grid(row=0, column=0, padx=5, pady=5) ttk.Radiobutton(object_frame, text="物在有限远", variable=self.object_var, value=False, command=self.toggle_object_input).grid(row=0, column=1, padx=5, pady=5) # 无穷远参数 self.infinite_frame = ttk.Frame(object_frame) self.infinite_frame.grid(row=1, column=0, columnspan=2, sticky="w", padx=5, pady=5) ttk.Label(self.infinite_frame, text="半视场角 (度):").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.field_angle_entry = ttk.Entry(self.infinite_frame, width=15) self.field_angle_entry.grid(row=0, column=1, padx=5, pady=5) # 有限远参数 (初始隐藏) self.finite_frame = ttk.Frame(object_frame) self.finite_frame.grid(row=1, column=0, columnspan=2, sticky="w", padx=5, pady=5) self.finite_frame.grid_remove() # 初始隐藏 ttk.Label(self.finite_frame, text="物距 (mm):").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.object_distance_entry = ttk.Entry(self.finite_frame, width=15) self.object_distance_entry.grid(row=0, column=1, padx=5, pady=5) ttk.Label(self.finite_frame, text="物高 (mm):").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.object_height_entry = ttk.Entry(self.finite_frame, width=15) self.object_height_entry.grid(row=0, column=3, padx=5, pady=5) ttk.Label(self.finite_frame, text="孔径角 (度):").grid(row=0, column=4, padx=5, pady=5, sticky="w") self.aperture_angle_entry = ttk.Entry(self.finite_frame, width=15) self.aperture_angle_entry.grid(row=0, column=5, padx=5, pady=5) # 光学表面输入 surface_frame = ttk.LabelFrame(parent, text="光学表面参数") surface_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 表面输入控件 input_frame = ttk.Frame(surface_frame) input_frame.pack(fill=tk.X, padx=5, pady=5) ttk.Label(input_frame, text="曲率半径 (mm):").grid(row=0, column=0, padx=5, pady=5) self.radius_entry = ttk.Entry(input_frame, width=10) self.radius_entry.grid(row=0, column=1, padx=5, pady=5) self.radius_entry.insert(0, "50") ttk.Label(input_frame, text="厚度 (mm):").grid(row=0, column=2, padx=5, pady=5) self.thickness_entry = ttk.Entry(input_frame, width=10) self.thickness_entry.grid(row=0, column=3, padx=5, pady=5) self.thickness_entry.insert(0, "5") ttk.Label(input_frame, text="折射率 (nd):").grid(row=0, column=4, padx=5, pady=5) self.nd_entry = ttk.Entry(input_frame, width=10) self.nd_entry.grid(row=0, column=5, padx=5, pady=5) self.nd_entry.insert(0, "1.5") ttk.Label(input_frame, text="阿贝数 (vd):").grid(row=0, column=6, padx=5, pady=5) self.vd_entry = ttk.Entry(input_frame, width=10) self.vd_entry.grid(row=0, column=7, padx=5, pady=5) self.vd_entry.insert(0, "60") button_frame = ttk.Frame(input_frame) button_frame.grid(row=0, column=8, padx=10) add_btn = ttk.Button(button_frame, text="添加表面", command=self.add_surface) add_btn.pack(side=tk.LEFT, padx=5) remove_btn = ttk.Button(button_frame, text="删除表面", command=self.remove_surface) remove_btn.pack(side=tk.LEFT, padx=5) # 表面列表 list_frame = ttk.Frame(surface_frame) list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) columns = ("#", "曲率半径 (mm)", "厚度 (mm)", "折射率 (nd)", "阿贝数 (vd)") self.surface_tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=8) for col in columns: self.surface_tree.heading(col, text=col) self.surface_tree.column(col, width=100, anchor=tk.CENTER) vsb = ttk.Scrollbar(list_frame, orient="vertical", command=self.surface_tree.yview) self.surface_tree.configure(yscrollcommand=vsb.set) self.surface_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) vsb.pack(side=tk.RIGHT, fill=tk.Y) def create_result_panel(self, parent): # 结果文本框 self.result_text = tk.Text(parent, wrap=tk.WORD) result_scroll_y = ttk.Scrollbar(parent, orient="vertical", command=self.result_text.yview) result_scroll_x = ttk.Scrollbar(parent, orient="horizontal", command=self.result_text.xview) self.result_text.configure(yscrollcommand=result_scroll_y.set, xscrollcommand=result_scroll_x.set) result_scroll_y.pack(side=tk.RIGHT, fill=tk.Y) result_scroll_x.pack(side=tk.BOTTOM, fill=tk.X) self.result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 设置初始文本 self.result_text.insert(tk.END, "计算结果将显示在此处...\n\n") self.result_text.configure(state=tk.DISABLED) def toggle_object_input(self): """切换物方参数输入界面""" if self.object_var.get(): # 无穷远 self.infinite_frame.grid() self.finite_frame.grid_remove() else: # 有限远 self.infinite_frame.grid_remove() self.finite_frame.grid() def add_surface(self): """添加光学表面""" try: # 获取输入值 r = self.radius_entry.get().strip() d = self.thickness_entry.get().strip() nd = self.nd_entry.get().strip() vd = self.vd_entry.get().strip() # 处理输入值 r = float(r) if r and r.lower() != "inf" else float('inf') d = float(d) if d else 0.0 nd = float(nd) if nd else 1.0 vd = float(vd) if vd else 0.0 # 添加到系统 surface = Surface(r, d, nd, vd) self.system.surfaces.append(surface) # 添加到树形视图 r_str = "平面" if r == float('inf') else f"{r:.2f}" self.surface_tree.insert("", "end", values=( len(self.system.surfaces), r_str, f"{d:.2f}", f"{nd:.4f}", f"{vd:.1f}" )) # 清空输入框 self.radius_entry.delete(0, tk.END) self.thickness_entry.delete(0, tk.END) self.nd_entry.delete(0, tk.END) self.vd_entry.delete(0, tk.END) self.radius_entry.focus_set() except ValueError: messagebox.showerror("输入错误", "请输入有效的数字") def remove_surface(self): """删除选中的光学表面""" selected = self.surface_tree.selection() if selected: # 从树形视图中删除 for item in selected: index = int(self.surface_tree.item(item, "values")[0]) - 1 self.surface_tree.delete(item) # 从系统中删除 if 0 <= index < len(self.system.surfaces): self.system.surfaces.pop(index) # 更新剩余表面的序号 for i, item in enumerate(self.surface_tree.get_children()): values = list(self.surface_tree.item(item, "values")) values[0] = i + 1 self.surface_tree.item(item, values=values) def load_system(self): """加载系统参数文件""" file_path = self.file_path_entry.get().strip() if not file_path: messagebox.showwarning("警告", "请输入文件路径") return try: with open(file_path, 'r') as f: data = json.load(f) # 加载系统参数 self.system.entrance_pupil_diameter = data.get("entrance_pupil_diameter") self.system.entrance_pupil_position = data.get("entrance_pupil_position") self.system.object_infinite = data.get("object_infinite", True) self.system.field_angle = data.get("field_angle") self.system.object_distance = data.get("object_distance") self.system.object_height = data.get("object_height") self.system.aperture_angle = data.get("aperture_angle") # 更新UI中的参数值 if self.system.entrance_pupil_diameter is not None: self.entrance_diameter_entry.delete(0, tk.END) self.entrance_diameter_entry.insert(0, str(self.system.entrance_pupil_diameter)) if self.system.entrance_pupil_position is not None: self.entrance_position_entry.delete(0, tk.END) self.entrance_position_entry.insert(0, str(self.system.entrance_pupil_position)) self.object_var.set(self.system.object_infinite) self.toggle_object_input() if self.system.field_angle is not None: self.field_angle_entry.delete(0, tk.END) self.field_angle_entry.insert(0, str(self.system.field_angle)) if self.system.object_distance is not None: self.object_distance_entry.delete(0, tk.END) self.object_distance_entry.insert(0, str(self.system.object_distance)) if self.system.object_height is not None: self.object_height_entry.delete(0, tk.END) self.object_height_entry.insert(0, str(self.system.object_height)) if self.system.aperture_angle is not None: self.aperture_angle_entry.delete(0, tk.END) self.aperture_angle_entry.insert(0, str(self.system.aperture_angle)) # 加载表面数据 self.system.surfaces = [] self.surface_tree.delete(*self.surface_tree.get_children()) surfaces_data = data.get("surfaces", []) for surf_data in surfaces_data: surface = Surface.from_dict(surf_data) self.system.surfaces.append(surface) r_str = "平面" if surface.r == float('inf') else f"{surface.r:.2f}" self.surface_tree.insert("", "end", values=( len(self.system.surfaces), r_str, f"{surface.d:.2f}", f"{surface.nd:.4f}", f"{surface.vd:.1f}" )) messagebox.showinfo("成功", f"系统参数已从 {os.path.basename(file_path)} 加载") # 显示加载的系统信息 self.result_text.configure(state=tk.NORMAL) self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, f"已加载系统参数文件: {file_path}\n") self.result_text.insert(tk.END, f"包含 {len(self.system.surfaces)} 个光学表面\n") self.result_text.configure(state=tk.DISABLED) except FileNotFoundError: messagebox.showerror("错误", f"文件不存在: {file_path}") except Exception as e: messagebox.showerror("加载错误", f"加载文件失败: {str(e)}") def save_system(self): """保存系统参数文件""" # 更新系统参数 if not self.update_system_from_ui(): return # 如果没有表面数据,提示用户 if not self.system.surfaces: messagebox.showwarning("警告", "没有光学表面数据,无法保存") return file_path = self.file_path_entry.get().strip() if not file_path: messagebox.showwarning("警告", "请输入保存路径") return try: # 准备保存数据 data = { "entrance_pupil_diameter": self.system.entrance_pupil_diameter, "entrance_pupil_position": self.system.entrance_pupil_position, "object_infinite": self.system.object_infinite, "field_angle": self.system.field_angle, "object_distance": self.system.object_distance, "object_height": self.system.object_height, "aperture_angle": self.system.aperture_angle, "surfaces": [surf.to_dict() for surf in self.system.surfaces] } with open(file_path, 'w') as f: json.dump(data, f, indent=4) messagebox.showinfo("成功", f"系统参数已保存到 {os.path.basename(file_path)}") # 显示保存信息 self.result_text.configure(state=tk.NORMAL) self.result_text.insert(tk.END, f"系统参数已保存到: {file_path}\n") self.result_text.configure(state=tk.DISABLED) except Exception as e: messagebox.showerror("保存错误", f"保存文件失败: {str(e)}") def save_results(self): """保存计算结果到文件""" if not self.system.results or all(v is None for v in self.system.results.values()): messagebox.showwarning("警告", "没有计算结果可保存") return file_path = filedialog.asksaveasfilename( title="保存计算结果", defaultextension=".txt", filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] ) if not file_path: return try: with open(file_path, 'w') as f: # 写入系统参数摘要 f.write("===== 光学系统参数 =====\n") f.write(f"入瞳直径: {self.system.entrance_pupil_diameter} mm\n") f.write(f"入瞳位置: {self.system.entrance_pupil_position} mm\n") f.write(f"色光类型: {self.system.light_type}\n") f.write("物距类型: " + ("无穷远" if self.system.object_infinite else "有限远") + "\n") if self.system.object_infinite: f.write(f"半视场角: {self.system.field_angle} 度\n") else: f.write(f"物距: {self.system.object_distance} mm\n") f.write(f"物高: {self.system.object_height} mm\n") f.write(f"孔径角: {self.system.aperture_angle} 度\n") f.write("\n光学表面参数:\n") for i, surf in enumerate(self.system.surfaces): r_str = "平面" if surf.r == float('inf') else f"{surf.r:.2f} mm" f.write(f"表面 {i+1}: r={r_str}, d={surf.d:.2f} mm, nd={surf.nd:.4f}, vd={surf.vd:.1f}\n") # 写入计算结果 f.write("\n\n===== 计算结果 =====\n") for key, value in self.system.results.items(): if value is not None: # 格式化键名 label = self.format_result_label(key) f.write(f"{label}: {value}\n") messagebox.showinfo("成功", f"计算结果已保存到 {os.path.basename(file_path)}") # 显示保存信息 self.result_text.configure(state=tk.NORMAL) self.result_text.insert(tk.END, f"计算结果已保存到: {file_path}\n") self.result_text.configure(state=tk.DISABLED) except Exception as e: messagebox.showerror("保存错误", f"保存计算结果失败: {str(e)}") def format_result_label(self, key): """格式化结果标签为中文描述""" labels = { "focal_length": "焦距 f' (mm)", "ideal_image_distance": "理想像距 l' (mm)", "actual_image_position": "实际像位置 (mm)", "image_principal_plane": "像方主面位置 lH' (mm)", "exit_pupil_distance": "出瞳距 lp' (mm)", "ideal_image_height": "理想像高 y0' (mm)", "spherical_aberration": "球差 (mm)", "longitudinal_chromatic": "位置色差 (mm)", "tangential_field_curvature": "子午场曲 xt' (mm)", "sagittal_field_curvature": "弧矢场曲 xs' (mm)", "astigmatism": "像散 Δxts' (mm)", "actual_image_height": "实际像高 (mm)", "relative_distortion": "相对畸变 (%)", "absolute_distortion": "绝对畸变 (mm)", "lateral_chromatic": "倍率色差 (mm)", "tangential_coma": "子午慧差 (mm)" } return labels.get(key, key) def update_system_from_ui(self): """从UI更新系统参数""" try: # 入瞳参数 if self.entrance_diameter_entry.get(): self.system.entrance_pupil_diameter = float(self.entrance_diameter_entry.get()) if self.entrance_position_entry.get(): self.system.entrance_pupil_position = float(self.entrance_position_entry.get()) # 色光类型 self.system.light_type = self.light_type_var.get() # 物方参数 self.system.object_infinite = self.object_var.get() if self.system.object_infinite: if self.field_angle_entry.get(): self.system.field_angle = float(self.field_angle_entry.get()) else: if self.object_distance_entry.get(): self.system.object_distance = float(self.object_distance_entry.get()) if self.object_height_entry.get(): self.system.object_height = float(self.object_height_entry.get()) if self.aperture_angle_entry.get(): self.system.aperture_angle = float(self.aperture_angle_entry.get()) except ValueError: messagebox.showerror("输入错误", "请输入有效的数字") return False return True def calculate(self): """执行光学计算""" # 更新系统参数 if not self.update_system_from_ui(): return # 检查必要参数 if not self.system.surfaces: messagebox.showwarning("警告", "请至少添加一个光学表面") return if self.system.entrance_pupil_diameter is None: messagebox.showwarning("警告", "请输入入瞳直径") return if self.system.object_infinite and self.system.field_angle is None: messagebox.showwarning("警告", "请输入半视场角") return if not self.system.object_infinite and ( self.system.object_distance is None or self.system.object_height is None or self.system.aperture_angle is None ): messagebox.showwarning("警告", "请输入完整的有限远参数") return # 执行计算 - 这里调用您的计算函数 self.perform_calculations() # 显示结果 self.display_results() # 显示计算完成消息 messagebox.showinfo("计算完成", "光学计算已完成,结果已显示在结果区域") def perform_calculations(self): """执行光学计算 - 这里调用您的实际计算函数""" # 重置结果 for key in self.system.results: self.system.results[key] = None # 示例:设置一些假结果用于演示 # 计算焦距 focal_length = calculate.calculate_focal_length(self.system.surfaces) self.system.results["focal_length"] = focal_length # 计算主面位置 principal_plane = calculate.calculate_principal_plane_position(self.system.surfaces) self.system.results["image_principal_plane"] = principal_plane # 计算理想像距 ideal_image_distance = calculate.calculate_ideal_image_distance( self.system.surfaces, self.system.object_infinite, self.system.object_distance ) self.system.results["ideal_image_distance"] = ideal_image_distance +principal_plane # 实际应用中,您需要调用您自己的计算函数 ##self.system.results["focal_length"] = 50.0 ##self.system.results["ideal_image_distance"] = 48.5 self.system.results["actual_image_position"] = 48.7 ##self.system.results["image_principal_plane"] = -1.2 self.system.results["exit_pupil_distance"] = 45.3 self.system.results["ideal_image_height"] = 10.2 self.system.results["spherical_aberration"] = 0.05 self.system.results["longitudinal_chromatic"] = 0.02 self.system.results["tangential_field_curvature"] = 0.15 self.system.results["sagittal_field_curvature"] = 0.12 self.system.results["astigmatism"] = 0.03 self.system.results["actual_image_height"] = 10.1 self.system.results["relative_distortion"] = -0.5 self.system.results["absolute_distortion"] = -0.05 self.system.results["lateral_chromatic"] = 0.01 self.system.results["tangential_coma"] = 0.04 def display_results(self): """在结果文本框中显示计算结果""" self.result_text.configure(state=tk.NORMAL) self.result_text.delete(1.0, tk.END) # 添加系统参数摘要 self.result_text.insert(tk.END, "===== 光学系统参数 =====\n", "title") self.result_text.insert(tk.END, f"入瞳直径: {self.system.entrance_pupil_diameter} mm\n") self.result_text.insert(tk.END, f"入瞳位置: {self.system.entrance_pupil_position} mm\n") self.result_text.insert(tk.END, "物距类型: " + ("无穷远" if self.system.object_infinite else "有限远") + "\n") self.result_text.insert(tk.END, f"色光类型: {self.system.light_type}\n") if self.system.object_infinite: self.result_text.insert(tk.END, f"半视场角: {self.system.field_angle} 度\n") else: self.result_text.insert(tk.END, f"物距: {self.system.object_distance} mm\n") self.result_text.insert(tk.END, f"物高: {self.system.object_height} mm\n") self.result_text.insert(tk.END, f"孔径角: {self.system.aperture_angle} 度\n") self.result_text.insert(tk.END, "\n光学表面参数:\n") for i, surf in enumerate(self.system.surfaces): r_str = "平面" if surf.r == float('inf') else f"{surf.r:.2f} mm" self.result_text.insert(tk.END, f"表面 {i+1}: r={r_str}, d={surf.d:.2f} mm, nd={surf.nd:.4f}, vd={surf.vd:.1f}\n") # 添加计算结果 self.result_text.insert(tk.END, "\n\n===== 计算结果 =====\n", "title") # 计算关键结果 key_results = [ "focal_length", "ideal_image_distance", "actual_image_position", "image_principal_plane", "exit_pupil_distance", "ideal_image_height" ] for key in key_results: if self.system.results[key] is not None: label = self.format_result_label(key) self.result_text.insert(tk.END, f"{label}: {self.system.results[key]}\n") # 添加像差结果 self.result_text.insert(tk.END, "\n像差分析:\n", "subtitle") aberrations = [ "spherical_aberration", "longitudinal_chromatic", "tangential_field_curvature", "sagittal_field_curvature", "astigmatism", "actual_image_height", "relative_distortion", "absolute_distortion", "lateral_chromatic", "tangential_coma" ] for key in aberrations: if self.system.results[key] is not None: label = self.format_result_label(key) self.result_text.insert(tk.END, f"{label}: {self.system.results[key]}\n") self.result_text.configure(state=tk.DISABLED) def clear_all(self): """清除所有输入和结果""" # 清除系统参数 self.system = OpticalSystem() # 清除UI输入 self.file_path_entry.delete(0, tk.END) self.entrance_diameter_entry.delete(0, tk.END) self.entrance_position_entry.delete(0, tk.END) self.field_angle_entry.delete(0, tk.END) self.object_distance_entry.delete(0, tk.END) self.object_height_entry.delete(0, tk.END) self.aperture_angle_entry.delete(0, tk.END) self.radius_entry.delete(0, tk.END) self.radius_entry.insert(0, "50") self.thickness_entry.delete(0, tk.END) self.thickness_entry.insert(0, "5") self.nd_entry.delete(0, tk.END) self.nd_entry.insert(0, "1.5") self.vd_entry.delete(0, tk.END) self.vd_entry.insert(0, "60") # 清除表面列表 self.surface_tree.delete(*self.surface_tree.get_children()) self.system.surfaces = [] # 清除结果 self.result_text.configure(state=tk.NORMAL) self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, "计算结果将显示在此处...\n\n") self.result_text.configure(state=tk.DISABLED) # 重置物距类型 self.object_var.set(True) self.toggle_object_input() messagebox.showinfo("清除完成", "所有输入和结果已清除") def load_default_settings(self): """加载默认设置(可选)""" # 这里可以添加加载默认设置的逻辑 pass if __name__ == "__main__": root = tk.Tk() app = MainApplication(root) # 设置文本样式 app.result_text.tag_config("title", font=("Arial", 10, "bold")) app.result_text.tag_config("subtitle", font=("Arial", 9, "bold")) root.mainloop() 完成出瞳距(以最后一个折射面为参考)、理想像高的计算
06-27
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值