[TreeView]使用总汇聚

本文详细介绍了TreeView控件的使用方法。包括组件安装,常用属性和方法,如Index、Nodes等。还给出实用技巧,如点击文字展开节点等。同时提供多个示例,如无级树、与数据库结合动态显示数据,以及客户端控制TreeView的JavaScript代码。

相信只要你仔细阅读了这篇小菜文,并参考一下提供的例程,一定能够掌握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[i];
    alert(cNode.getAttribute("Text"));
    if(parseInt(cNode.getChildren().length)!=0)
     AlertNode(cNode.getChildren());   
   }
  }
 }


这里是微软的教程:http://www.microsoft.com/china/MSDN/library/archives/library/DNAspp/html/aspnet-usingtreeviewieWebcontrol.asp

现在我的功能都能实现,但是最近上了一台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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值