import tkinter as tk
from tkinter import ttk, simpledialog, messagebox, filedialog
import pandas as pd
import subprocess
import queue
from datetime import datetime
import concurrent.futures
import time
import os
import re
from PIL import Image, ImageDraw, ImageFont
import numpy as np
class SettingsDialog(tk.Toplevel):
"""设置对话框"""
def __init__(self, parent, current_settings):
super().__init__(parent)
self.title("监控设置")
self.geometry("400x350")
self.parent = parent
self.settings = current_settings.copy()
self.result = None
# 使对话框模态化
self.transient(parent)
self.grab_set()
# 创建表单框架
form_frame = tk.Frame(self, padx=10, pady=10)
form_frame.pack(fill=tk.BOTH, expand=True)
# 线程数设置
tk.Label(form_frame, text="并发线程数 (1-1000):").grid(row=0, column=0, sticky=tk.W, pady=5)
self.threads_var = tk.IntVar(value=self.settings['thread_count'])
tk.Spinbox(form_frame, from_=1, to=1000, textvariable=self.threads_var, width=10).grid(row=0, column=1,
sticky=tk.W, padx=5)
# Ping间隔设置
tk.Label(form_frame, text="Ping间隔 (秒):").grid(row=1, column=0, sticky=tk.W, pady=5)
self.interval_var = tk.DoubleVar(value=self.settings['ping_interval'])
tk.Spinbox(form_frame, from_=0.1, to=10, increment=0.1,
textvariable=self.interval_var, width=10).grid(row=1, column=1, sticky=tk.W, padx=5)
# Ping超时设置
tk.Label(form_frame, text="Ping超时 (毫秒):").grid(row=2, column=0, sticky=tk.W, pady=5)
self.timeout_var = tk.IntVar(value=self.settings['ping_timeout'])
tk.Spinbox(form_frame, from_=100, to=5000, increment=100,
textvariable=self.timeout_var, width=10).grid(row=2, column=1, sticky=tk.W, padx=5)
# Excel文件路径
tk.Label(form_frame, text="Excel文件路径:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.excel_path_var = tk.StringVar(value=self.settings['excel_path'])
tk.Entry(form_frame, textvariable=self.excel_path_var, width=30).grid(row=3, column=1, sticky=tk.W, padx=5)
tk.Button(form_frame, text="浏览...", command=self.browse_excel).grid(row=3, column=2, padx=5)
# 断开计数阈值
tk.Label(form_frame, text="断开报警阈值:").grid(row=4, column=0, sticky=tk.W, pady=5)
self.threshold_var = tk.IntVar(value=self.settings['disconnect_threshold'])
tk.Spinbox(form_frame, from_=1, to=100, increment=1,
textvariable=self.threshold_var, width=10).grid(row=4, column=1, sticky=tk.W, padx=5)
# 关键设备类型
tk.Label(form_frame, text="关键设备类型:").grid(row=5, column=0, sticky=tk.W, pady=5)
self.critical_types_var = tk.StringVar(value=",".join(self.settings['critical_types']))
tk.Entry(form_frame, textvariable=self.critical_types_var, width=30).grid(row=5, column=1, columnspan=2,
sticky=tk.W, padx=5)
tk.Label(form_frame, text="(逗号分隔,如:二层交换机,三层交换机,服务器,UPS)").grid(row=6, column=1, sticky=tk.W,
padx=5)
# 按钮框架
button_frame = tk.Frame(self)
button_frame.pack(pady=10)
tk.Button(button_frame, text="确定", width=10, command=self.ok).grid(row=0, column=0, padx=10)
tk.Button(button_frame, text="取消", width=10, command=self.cancel).grid(row=0, column=1, padx=10)
self.protocol("WM_DELETE_WINDOW", self.cancel)
def browse_excel(self):
"""浏览Excel文件"""
file_path = filedialog.askopenfilename(
filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")],
title="选择设备列表Excel文件"
)
if file_path:
self.excel_path_var.set(file_path)
def ok(self):
"""确定按钮处理"""
self.settings = {
'thread_count': self.threads_var.get(),
'ping_interval': self.interval_var.get(),
'ping_timeout': self.timeout_var.get(),
'excel_path': self.excel_path_var.get(),
'disconnect_threshold': self.threshold_var.get(),
'critical_types': [t.strip() for t in self.critical_types_var.get().split(",")]
}
self.destroy()
def cancel(self):
"""取消按钮处理"""
self.settings = None
self.destroy()
def show(self):
"""显示对话框并返回设置"""
self.wait_window()
return self.settings
class PingMonitorApp:
def __init__(self, root):
self.root = root
self.root.title("IP Ping 监控工具 - 专业版")
self.root.geometry("1100x700") # 窗口大小
# 控制面板框架
control_frame = tk.Frame(root)
control_frame.pack(fill=tk.X, padx=10, pady=5)
# 操作按钮框架
button_frame = tk.Frame(control_frame)
button_frame.pack(side=tk.LEFT)
# 开始/停止按钮
self.start_btn = tk.Button(button_frame, text="开始监控",
command=self.toggle_monitoring, width=10)
self.start_btn.pack(side=tk.LEFT, padx=5)
# 设置按钮
self.settings_btn = tk.Button(button_frame, text="设置",
command=self.open_settings, width=10)
self.settings_btn.pack(side=tk.LEFT, padx=5)
# 导出按钮
export_frame = tk.Frame(control_frame)
export_frame.pack(side=tk.LEFT, padx=20)
tk.Label(export_frame, text="导出断网设备:").pack(side=tk.LEFT)
self.export_excel_btn = tk.Button(export_frame, text="Excel",
command=self.export_disconnected_to_excel, width=8)
self.export_excel_btn.pack(side=tk.LEFT, padx=2)
self.export_txt_btn = tk.Button(export_frame, text="TXT",
command=self.export_disconnected_to_txt, width=8)
self.export_txt_btn.pack(side=tk.LEFT, padx=2)
self.export_img_btn = tk.Button(export_frame, text="图片",
command=self.export_disconnected_to_image, width=8)
self.export_img_btn.pack(side=tk.LEFT, padx=2)
# 状态指示框架
status_frame = tk.Frame(control_frame)
status_frame.pack(side=tk.RIGHT)
# 状态指示器
self.status_indicator = tk.Canvas(status_frame, width=20, height=20)
self.status_indicator.pack(side=tk.LEFT, padx=5)
self.draw_indicator("gray") # 初始灰色
# 速度显示
self.speed_label = tk.Label(status_frame, text="速度: --")
self.speed_label.pack(side=tk.LEFT, padx=10)
# 断开设备计数
self.disconnected_count = tk.IntVar(value=0)
tk.Label(status_frame, text="断开设备:").pack(side=tk.LEFT)
self.disconnected_label = tk.Label(status_frame, textvariable=self.disconnected_count)
self.disconnected_label.pack(side=tk.LEFT, padx=5)
# 创建表格
self.tree = ttk.Treeview(root)
self.tree["columns"] = ("#1", "#2", "#3", "#4", "#5", "#6", "#7", "#8")
self.tree.column("#0", width=0, stretch=tk.NO) # 隐藏首列
self.tree.column("#1", width=50, anchor=tk.CENTER) # 序号
self.tree.column("#2", width=160, anchor=tk.CENTER) # 时间戳
self.tree.column("#3", width=120, anchor=tk.CENTER) # sheet页名
self.tree.column("#4", width=120, anchor=tk.CENTER) # 设备IP
self.tree.column("#5", width=150, anchor=tk.CENTER) # 车站
self.tree.column("#6", width=200, anchor=tk.CENTER) # 设备名称
self.tree.column("#7", width=100, anchor=tk.CENTER) # 断开次数
self.tree.column("#8", width=150, anchor=tk.CENTER) # Ping结果
self.tree.heading("#1", text="序号")
self.tree.heading("#2", text="时间戳")
self.tree.heading("#3", text="sheet页名")
self.tree.heading("#4", text="设备IP")
self.tree.heading("#5", text="车站")
self.tree.heading("#6", text="设备名称")
self.tree.heading("#7", text="断开次数")
self.tree.heading("#8", text="Ping结果")
self.tree.pack(fill=tk.BOTH, expand=True)
# 配置标签颜色
self.tree.tag_configure("critical", background="#ffffcc") # 关键设备断开 - 黄色
self.tree.tag_configure("failed", background="#ffe6e6") # 普通设备超过阈值 - 红色
# 状态栏
self.status_var = tk.StringVar()
self.status_bar = tk.Label(root, textvariable=self.status_var,
bd=1, relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# 创建线程安全队列
self.results_queue = queue.Queue()
self.device_queue = queue.Queue()
# 存储所有设备信息
self.devices = []
self.device_map = {} # IP到设备的映射
# 线程控制
self.running = False
self.thread_pool = None
self.executor = None
# 默认设置
self.settings = {
'thread_count': 100,
'ping_interval': 1.0,
'ping_timeout': 300, # 毫秒
'excel_path': "D:/1.xlsx",
'disconnect_threshold': 5, # 断开次数报警阈值
'critical_types': ['二层交换机', '三层交换机', '服务器', 'UPS'] # 关键设备类型
}
# 性能统计
self.last_update_time = time.time()
self.ping_count = 0
# 加载IP列表
self.load_ips()
# GUI更新循环
self.update_gui()
# 关闭窗口时清理资源
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def draw_indicator(self, color):
"""绘制状态指示灯"""
self.status_indicator.delete("all")
self.status_indicator.create_oval(2, 2, 18, 18, fill=color, outline="black")
def open_settings(self):
"""打开设置对话框"""
# 如果正在运行,先停止监控
was_running = self.running
if was_running:
self.stop_monitoring()
# 打开设置对话框
dialog = SettingsDialog(self.root, self.settings)
new_settings = dialog.show()
if new_settings:
# 应用新设置
self.settings = new_settings
self.status_var.set("设置已更新")
# 重新加载设备列表
self.load_ips()
# 如果之前正在运行,重新启动监控
if was_running:
self.start_monitoring()
def toggle_monitoring(self):
"""切换监控状态"""
if self.running:
self.stop_monitoring()
self.start_btn.config(text="开始监控")
self.draw_indicator("red")
self.status_var.set("监控已停止")
else:
self.start_monitoring()
self.start_btn.config(text="停止监控")
self.draw_indicator("green")
self.status_var.set("监控已启动")
def is_critical_device(self, device_name):
"""检查设备是否为关键设备"""
device_name = str(device_name).lower()
for device_type in self.settings['critical_types']:
if device_type.lower() in device_name:
return True
return False
def load_ips(self):
"""从Excel文件加载设备信息"""
try:
# 读取Excel文件
excel_path = self.settings['excel_path']
if not os.path.exists(excel_path):
self.status_var.set(f"错误:文件不存在 - {excel_path}")
return
xls = pd.ExcelFile(excel_path)
# 存储所有设备
self.devices = []
self.device_map = {}
device_counter = 1
# 遍历所有sheet页
for sheet_name in xls.sheet_names:
df = pd.read_excel(excel_path, sheet_name=sheet_name)
# 检查必要列是否存在
required_columns = ["设备IP", "车站", "设备名称"]
if not all(col in df.columns for col in required_columns):
self.status_var.set(f"错误:Sheet '{sheet_name}' 缺少必要列")
continue
# 提取设备信息
for _, row in df.iterrows():
ip = row["设备IP"]
device_name = row["设备名称"]
# 检查设备是否为关键设备
is_critical = self.is_critical_device(device_name)
device_info = {
"id": device_counter,
"sheet": sheet_name,
"ip": ip,
"station": row["车站"],
"device_name": device_name,
"is_critical": is_critical, # 标记关键设备
"status": "等待开始...",
"timestamp": "",
"success": None,
"last_failed": False, # 记录最近一次是否失败
"disconnect_count": 0, # 断开次数计数器
"last_ping_time": 0, # 上次Ping时间
"last_status_change": None # 上次状态变更时间
}
self.devices.append(device_info)
self.device_map[ip] = device_info
device_counter += 1
self.status_var.set(f"成功加载 {len(self.devices)} 个设备,点击开始按钮开始监控")
# 清空设备队列
while not self.device_queue.empty():
self.device_queue.get_nowait()
# 清空表格并添加设备
self.tree.delete(*self.tree.get_children())
for device in self.devices:
self.tree.insert("", "end", values=(
device["id"],
device["timestamp"],
device["sheet"],
device["ip"],
device["station"],
device["device_name"],
device["disconnect_count"],
device["status"]
))
# 添加设备到队列
self.device_queue.put(device["ip"])
except Exception as e:
self.status_var.set(f"加载Excel文件错误: {str(e)}")
def start_monitoring(self):
"""启动监控服务"""
if not self.running:
self.running = True
self.executor = concurrent.futures.ThreadPoolExecutor(
max_workers=self.settings['thread_count'])
# 启动线程池工作线程
for _ in range(self.settings['thread_count']):
self.executor.submit(self.ping_worker)
# 更新设备状态
for device in self.devices:
device["status"] = "监控中..."
self.status_var.set(f"监控已启动 ({self.settings['thread_count']}线程)")
def stop_monitoring(self):
"""停止监控服务"""
if self.running:
self.running = False
if self.executor:
self.executor.shutdown(wait=False)
# 更新设备状态
for device in self.devices:
device["status"] = "已停止"
self.status_var.set("监控已停止")
def ping_worker(self):
"""工作线程:执行Ping操作"""
while self.running:
try:
# 从队列获取设备IP
ip = self.device_queue.get(timeout=1.0)
# 检查设备是否存在
if ip not in self.device_map:
self.device_queue.put(ip)
continue
device = self.device_map[ip]
# 检查是否需要Ping(根据时间间隔)
current_time = time.time()
if current_time - device["last_ping_time"] < self.settings['ping_interval']:
# 还没到时间,放回队列等待
self.device_queue.put(ip)
time.sleep(0.01) # 短暂休眠避免忙等待
continue
# 更新最后Ping时间
device["last_ping_time"] = current_time
# 执行Ping命令
timeout_ms = self.settings['ping_timeout']
result = subprocess.run(
["ping", "-n", "1", "-w", str(timeout_ms), str(ip)],
capture_output=True,
text=True
)
# 解析结果
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
success = "请求超时" not in result.stdout and "无法访问" not in result.stdout
status = "成功" if success else "失败"
# 更新性能计数
self.ping_count += 1
# 将结果放入队列
self.results_queue.put((ip, timestamp, status, success))
# 将设备放回队列以继续监控
self.device_queue.put(ip)
except queue.Empty:
# 如果队列为空,短暂休眠后重试
time.sleep(0.1)
except Exception as e:
self.results_queue.put((None, None, f"错误: {str(e)}", False))
time.sleep(1.0) # 出错时休眠避免频繁错误
def update_gui(self):
"""更新GUI界面,确保不通的设备显示在最上方"""
try:
# 更新性能统计
if self.running:
current_time = time.time()
if current_time - self.last_update_time >= 1.0: # 每秒更新一次速度
speed = self.ping_count / (current_time - self.last_update_time)
self.speed_label.config(text=f"速度: {speed:.1f} Ping/秒")
self.last_update_time = current_time
self.ping_count = 0
# 处理结果队列
disconnected_devices = 0
while not self.results_queue.empty():
ip, timestamp, status, success = self.results_queue.get_nowait()
if ip and ip in self.device_map:
device = self.device_map[ip]
# 记录状态变更
if success != device["success"]:
device["last_status_change"] = timestamp
# 更新断开次数
if not success:
device["disconnect_count"] += 1
device["timestamp"] = timestamp
device["status"] = status
device["success"] = success
device["last_failed"] = not success
self.results_queue.task_done()
# 重新排序设备:不通的设备在前,通的在后
self.devices.sort(key=lambda x: (not x["last_failed"], x["id"]))
# 清空表格并重新添加所有设备(按新顺序)
self.tree.delete(*self.tree.get_children())
disconnected_devices = 0
for device in self.devices:
if not device["success"]:
disconnected_devices += 1
# 根据设备状态和类型设置标签
tags = ()
if not device["success"]: # 设备断开
if device["is_critical"]: # 关键设备断开(黄色)
tags = ("critical",)
elif device["disconnect_count"] >= self.settings['disconnect_threshold']: # 普通设备超过阈值(红色)
tags = ("failed",)
self.tree.insert("", "end", values=(
device["id"],
device["timestamp"],
device["sheet"],
device["ip"],
device["station"],
device["device_name"],
device["disconnect_count"],
device["status"]
), tags=tags)
# 更新断开设备计数
self.disconnected_count.set(disconnected_devices)
except queue.Empty:
pass
except Exception as e:
self.status_var.set(f"GUI更新错误: {str(e)}")
# 每100毫秒检查一次更新
self.root.after(100, self.update_gui)
def on_close(self):
"""关闭窗口时的清理操作"""
self.running = False
if self.executor:
self.executor.shutdown(wait=False)
self.root.destroy()
def get_disconnected_devices(self):
"""获取所有断网设备列表"""
return [device for device in self.devices if not device["success"]]
def export_disconnected_to_excel(self):
"""导出断网设备到Excel文件(已取消关键设备列)"""
disconnected = self.get_disconnected_devices()
if not disconnected:
messagebox.showinfo("无断网设备", "当前没有断网设备")
return
try:
# 创建DataFrame
data = []
for device in disconnected:
data.append({
"序号": device["id"],
"时间戳": device["timestamp"],
"Sheet页名": device["sheet"],
"设备IP": device["ip"],
"车站": device["station"],
"设备名称": device["device_name"],
"断开次数": device["disconnect_count"],
"状态": device["status"]
})
df = pd.DataFrame(data)
# 弹出保存对话框
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
title="保存断网设备列表"
)
if not file_path: # 用户取消
return
# 保存到Excel
df.to_excel(file_path, index=False)
self.status_var.set(f"成功导出 {len(disconnected)} 个断网设备到 {file_path}")
except Exception as e:
messagebox.showerror("导出错误", f"导出Excel时出错: {str(e)}")
def export_disconnected_to_txt(self):
"""导出断网设备到TXT文件(已取消关键设备列)"""
disconnected = self.get_disconnected_devices()
if not disconnected:
messagebox.showinfo("无断网设备", "当前没有断网设备")
return
try:
# 弹出保存对话框
file_path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
title="保存断网设备列表"
)
if not file_path: # 用户取消
return
# 写入文件
with open(file_path, "w", encoding="utf-8") as f:
# 写入标题行
f.write("序号\t时间戳\tSheet页名\t设备IP\t车站\t设备名称\t断开次数\t状态\n")
# 写入设备数据
for device in disconnected:
f.write(f"{device['id']}\t{device['timestamp']}\t{device['sheet']}\t")
f.write(f"{device['ip']}\t{device['station']}\t{device['device_name']}\t")
f.write(f"{device['disconnect_count']}\t{device['status']}\n")
self.status_var.set(f"成功导出 {len(disconnected)} 个断网设备到 {file_path}")
except Exception as e:
messagebox.showerror("导出错误", f"导出TXT时出错: {str(e)}")
def export_disconnected_to_image(self):
"""导出断网设备到图片文件(已取消关键设备列)"""
disconnected = self.get_disconnected_devices()
if not disconnected:
messagebox.showinfo("无断网设备", "当前没有断网设备")
return
try:
# 弹出保存对话框
file_path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG 图片", "*.png"), ("JPEG 图片", "*.jpg"), ("所有文件", "*.*")],
title="保存断网设备列表图片"
)
if not file_path: # 用户取消
return
# 创建图片
columns = ["序号", "时间戳", "Sheet页名", "设备IP", "车站", "设备名称", "断开次数", "状态"]
row_height = 30
col_widths = [50, 160, 120, 120, 150, 200, 100, 150]
header_height = 40
# 计算图片尺寸
width = sum(col_widths) + 20
height = header_height + len(disconnected) * row_height + 40
# 创建白色背景图片
img = Image.new("RGB", (width, height), "white")
draw = ImageDraw.Draw(img)
try:
# 尝试加载系统字体
font = ImageFont.truetype("simsun.ttc", 12) # 宋体
header_font = ImageFont.truetype("simsun.ttc", 14, encoding="unic")
except:
# 回退到默认字体
font = ImageFont.load_default()
header_font = ImageFont.load_default()
# 绘制标题
draw.text((10, 10), f"断网设备列表 - 共 {len(disconnected)} 台", fill="black", font=header_font)
# 绘制表头
x = 10
for i, col in enumerate(columns):
draw.rectangle([x, header_height, x + col_widths[i], header_height + row_height], outline="black")
draw.text((x + 5, header_height + 5), col, fill="black", font=font)
x += col_widths[i]
# 绘制设备行
y = header_height + row_height
for device in disconnected:
x = 10
values = [
str(device["id"]),
device["timestamp"],
device["sheet"],
device["ip"],
device["station"],
device["device_name"],
str(device["disconnect_count"]),
device["status"]
]
# 根据设备类型设置行背景色
bg_color = "white"
if not device["success"]:
if device["is_critical"]:
bg_color = "#ffffcc" # 黄色
elif device["disconnect_count"] >= self.settings['disconnect_threshold']:
bg_color = "#ffe6e6" # 红色
# 绘制行背景
draw.rectangle([x, y, width - 10, y + row_height], fill=bg_color)
# 绘制单元格内容
for i, value in enumerate(values):
draw.rectangle([x, y, x + col_widths[i], y + row_height], outline="black")
draw.text((x + 5, y + 5), str(value), fill="black", font=font)
x += col_widths[i]
y += row_height
# 保存图片
img.save(file_path)
self.status_var.set(f"成功导出 {len(disconnected)} 个断网设备图片到 {file_path}")
except Exception as e:
messagebox.showerror("导出错误", f"导出图片时出错: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = PingMonitorApp(root)
root.mainloop()
添加窗口排序功能
最新发布