OK
This commit is contained in:
+47
-88
@@ -1,112 +1,71 @@
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
from tkinter import ttk
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BasicUI:
|
||||
"""旧版页面的兼容辅助类。
|
||||
|
||||
新界面主要使用 core.ui.components;保留这个类是为了兼容后续可能仍引用
|
||||
add_input/add_combobox/add_button 的小工具页。
|
||||
"""
|
||||
|
||||
def add_input(
|
||||
self, parent, label_text,
|
||||
row, col=0, inivar="",
|
||||
label_width=8, entry_width=20, colspan=1, sticky='w'
|
||||
):
|
||||
"""
|
||||
添加一个带文本标签和输入框的组合控件,并返回 (StringVar, Frame) 以便后续控制。
|
||||
- parent: 父容器
|
||||
- label: 标签文字
|
||||
- row, col: 放置在父容器的 grid 行列
|
||||
- inivar: 初始值
|
||||
- entry_width: 输入框宽度
|
||||
- colspan: 该组控件在父容器上跨越的列数
|
||||
- sticky: 对齐方式(默认左对齐)
|
||||
"""
|
||||
self,
|
||||
parent,
|
||||
label_text,
|
||||
row,
|
||||
col=0,
|
||||
inivar="",
|
||||
label_width=8,
|
||||
entry_width=20,
|
||||
colspan=1,
|
||||
sticky="w",
|
||||
):
|
||||
group_frame = ttk.Frame(parent)
|
||||
group_frame.grid(row=row, column=col, columnspan=colspan, sticky=sticky, padx=5, pady=3)
|
||||
|
||||
# 标签
|
||||
label = ttk.Label(group_frame, text=label_text + ":", width=label_width, anchor='w').grid(row=0, column=0, sticky='w', padx=(0, 5))
|
||||
label = ttk.Label(group_frame, text=f"{label_text}:", width=label_width, anchor="w")
|
||||
label.grid(row=0, column=0, sticky="w", padx=(0, 5))
|
||||
|
||||
# 输入框
|
||||
var = tk.StringVar(value=inivar)
|
||||
entry = ttk.Entry(group_frame, textvariable=var, width=entry_width)
|
||||
entry.grid(row=0, column=1, sticky='w')
|
||||
entry.grid(row=0, column=1, sticky="w")
|
||||
|
||||
return {
|
||||
"frame": group_frame,
|
||||
"label": label,
|
||||
"entry": entry,
|
||||
"var": var
|
||||
}
|
||||
return {"frame": group_frame, "label": label, "entry": entry, "var": var}
|
||||
|
||||
def add_combobox(
|
||||
self, parent, label_text, row, col=0,
|
||||
listbox=[], inivar=0, width=17, colspan=1,
|
||||
sticky='w', label_width=8, state="readonly"
|
||||
self,
|
||||
parent,
|
||||
label_text,
|
||||
row,
|
||||
col=0,
|
||||
listbox=None,
|
||||
inivar=0,
|
||||
width=17,
|
||||
colspan=1,
|
||||
sticky="w",
|
||||
label_width=8,
|
||||
state="readonly",
|
||||
):
|
||||
"""
|
||||
创建一组 [标签 + 下拉框] 控件。
|
||||
返回 dict,方便外部单独或统一控制。
|
||||
|
||||
- values: 下拉选项列表
|
||||
- default: 初始值(可选)
|
||||
- state: "readonly" 表示只能从列表选,"normal" 可手动输入
|
||||
"""
|
||||
values = listbox or []
|
||||
frame = ttk.Frame(parent)
|
||||
frame.grid(row=row, column=col, columnspan=colspan, sticky=sticky, padx=5, pady=3)
|
||||
|
||||
# 标签
|
||||
label = ttk.Label(
|
||||
frame,
|
||||
text=label_text + ":",
|
||||
width=label_width,
|
||||
anchor='w'
|
||||
)
|
||||
label.grid(row=0, column=0, sticky='w', padx=(0, 5))
|
||||
label = ttk.Label(frame, text=f"{label_text}:", width=label_width, anchor="w")
|
||||
label.grid(row=0, column=0, sticky="w", padx=(0, 5))
|
||||
|
||||
# 变量 + 下拉框
|
||||
var = tk.StringVar()
|
||||
combobox = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=var,
|
||||
values=listbox,
|
||||
width=width,
|
||||
state=state
|
||||
)
|
||||
combobox.grid(row=0, column=1, sticky='w')
|
||||
if inivar>=0:
|
||||
var.set(listbox[inivar])
|
||||
combobox = ttk.Combobox(frame, textvariable=var, values=values, width=width, state=state)
|
||||
combobox.grid(row=0, column=1, sticky="w")
|
||||
if values and inivar >= 0:
|
||||
var.set(values[inivar])
|
||||
|
||||
return {
|
||||
"frame": frame,
|
||||
"label": label,
|
||||
"combobox": combobox,
|
||||
"var": var
|
||||
}
|
||||
return {"frame": frame, "label": label, "combobox": combobox, "var": var}
|
||||
|
||||
def add_button(
|
||||
self, parent, button_text,
|
||||
row, col=0, command="",
|
||||
width=5, colspan=1, sticky='w'
|
||||
):
|
||||
"""
|
||||
添加一个按钮,并返回 (StringVar, Frame) 以便后续控制。
|
||||
- parent: 父容器
|
||||
- button_text: 按钮文本
|
||||
- row, col: 放置在父容器的 grid 行列
|
||||
- command: 调用的函数
|
||||
- width: 按钮宽度
|
||||
- colspan: 该组控件在父容器上跨越的列数
|
||||
- sticky: 对齐方式(默认左对齐)
|
||||
"""
|
||||
group_frame = ttk.Frame(parent)
|
||||
group_frame.grid(row=row, column=col, columnspan=colspan, sticky=sticky, padx=5, pady=3)
|
||||
def add_button(self, parent, button_text, row, col=0, command=None, width=5, colspan=1, sticky="w"):
|
||||
group_frame = ttk.Frame(parent)
|
||||
group_frame.grid(row=row, column=col, columnspan=colspan, sticky=sticky, padx=5, pady=3)
|
||||
|
||||
btn = ttk.Button(group_frame, text=button_text, command=command, width=width)
|
||||
btn.grid(row=0, column=0, sticky='w')
|
||||
return {
|
||||
"frame": group_frame,
|
||||
"btn": btn,
|
||||
}
|
||||
|
||||
btn = ttk.Button(group_frame, text=button_text, command=command, width=width)
|
||||
btn.grid(row=0, column=0, sticky="w")
|
||||
return {"frame": group_frame, "btn": btn}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import tkinter as tk
|
||||
from tkinter import scrolledtext, ttk
|
||||
from typing import Callable, Optional
|
||||
|
||||
from core.ui.theme import COLORS, FONT_MONO, FONT_SECTION, FONT_SMALL
|
||||
|
||||
|
||||
class Console:
|
||||
def __init__(self, parent, height: int = 16):
|
||||
self.widget = scrolledtext.ScrolledText(
|
||||
parent,
|
||||
height=height,
|
||||
wrap="word",
|
||||
bg=COLORS["console_bg"],
|
||||
fg=COLORS["console_fg"],
|
||||
insertbackground=COLORS["console_fg"],
|
||||
selectbackground="#2b4a6f",
|
||||
relief="flat",
|
||||
borderwidth=0,
|
||||
font=FONT_MONO,
|
||||
padx=14,
|
||||
pady=12,
|
||||
)
|
||||
self.widget.tag_config("muted", foreground=COLORS["console_muted"])
|
||||
self.widget.tag_config("success", foreground="#86efac")
|
||||
self.widget.tag_config("warning", foreground="#fde68a")
|
||||
self.widget.tag_config("error", foreground="#fca5a5")
|
||||
|
||||
def grid(self, **kwargs):
|
||||
self.widget.grid(**kwargs)
|
||||
|
||||
def pack(self, **kwargs):
|
||||
self.widget.pack(**kwargs)
|
||||
|
||||
def clear(self) -> None:
|
||||
self.widget.delete("1.0", tk.END)
|
||||
|
||||
def write(self, text: str, tag: Optional[str] = None) -> None:
|
||||
def append():
|
||||
self.widget.insert(tk.END, text, tag)
|
||||
self.widget.see(tk.END)
|
||||
|
||||
self.widget.after(0, append)
|
||||
|
||||
|
||||
class Page(ttk.Frame):
|
||||
def __init__(self, parent, title: str, subtitle: str):
|
||||
super().__init__(parent, style="Panel.TFrame")
|
||||
self.columnconfigure(0, weight=1)
|
||||
self.rowconfigure(1, weight=1)
|
||||
|
||||
header = ttk.Frame(self, style="Panel.TFrame")
|
||||
header.grid(row=0, column=0, sticky="ew", padx=28, pady=(24, 8))
|
||||
header.columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(header, text=title, style="Title.TLabel").grid(row=0, column=0, sticky="w")
|
||||
ttk.Label(header, text=subtitle, style="Muted.TLabel").grid(row=1, column=0, sticky="w", pady=(4, 0))
|
||||
|
||||
body_shell = ttk.Frame(self, style="Panel.TFrame")
|
||||
body_shell.grid(row=1, column=0, sticky="nsew", padx=28, pady=(4, 20))
|
||||
body_shell.columnconfigure(0, weight=1)
|
||||
body_shell.rowconfigure(0, weight=1)
|
||||
|
||||
self._canvas = tk.Canvas(body_shell, bg=COLORS["panel"], highlightthickness=0, bd=0)
|
||||
self._canvas.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
scrollbar = ttk.Scrollbar(body_shell, orient="vertical", command=self._canvas.yview)
|
||||
scrollbar.grid(row=0, column=1, sticky="ns", padx=(8, 0))
|
||||
self._canvas.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.body = ttk.Frame(self._canvas, style="Panel.TFrame")
|
||||
self.body.columnconfigure(0, weight=1)
|
||||
self._body_window = self._canvas.create_window((0, 0), window=self.body, anchor="nw")
|
||||
|
||||
self.body.bind("<Configure>", self._update_scroll_region)
|
||||
self._canvas.bind("<Configure>", self._resize_body)
|
||||
self._bind_mousewheel(self._canvas)
|
||||
|
||||
def section(self, title: str, row: int, columns: int = 4):
|
||||
frame = tk.Frame(self.body, bg=COLORS["panel"], highlightbackground=COLORS["border"], highlightthickness=1)
|
||||
frame.grid(row=row, column=0, sticky="ew", pady=(0, 14))
|
||||
for col in range(columns):
|
||||
frame.columnconfigure(col, weight=1)
|
||||
|
||||
ttk.Label(frame, text=title, style="Section.TLabel").grid(
|
||||
row=0, column=0, columnspan=columns, sticky="w", padx=18, pady=(14, 8)
|
||||
)
|
||||
return frame
|
||||
|
||||
def _update_scroll_region(self, _event=None) -> None:
|
||||
self._canvas.configure(scrollregion=self._canvas.bbox("all"))
|
||||
|
||||
def _resize_body(self, event) -> None:
|
||||
self._canvas.itemconfigure(self._body_window, width=event.width)
|
||||
|
||||
def _bind_mousewheel(self, widget) -> None:
|
||||
widget.bind("<Enter>", lambda _event: widget.bind_all("<MouseWheel>", self._on_mousewheel))
|
||||
widget.bind("<Leave>", lambda _event: widget.unbind_all("<MouseWheel>"))
|
||||
|
||||
def _on_mousewheel(self, event) -> None:
|
||||
if not self.winfo_ismapped():
|
||||
return
|
||||
self._canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||
|
||||
|
||||
def field(parent, label: str, row: int, column: int, value: str = "", width: int = 24, colspan: int = 1):
|
||||
frame = ttk.Frame(parent, style="Panel.TFrame")
|
||||
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=18, pady=(4, 14))
|
||||
frame.columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
|
||||
var = tk.StringVar(value=value)
|
||||
entry = ttk.Entry(frame, textvariable=var, width=width)
|
||||
entry.grid(row=1, column=0, sticky="ew")
|
||||
return {"frame": frame, "var": var, "entry": entry}
|
||||
|
||||
|
||||
def combo(parent, label: str, row: int, column: int, values=None, value: str = "", width: int = 24, colspan: int = 1):
|
||||
values = values or []
|
||||
frame = ttk.Frame(parent, style="Panel.TFrame")
|
||||
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=18, pady=(4, 14))
|
||||
frame.columnconfigure(0, weight=1)
|
||||
|
||||
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
|
||||
var = tk.StringVar(value=value)
|
||||
control = ttk.Combobox(frame, textvariable=var, values=values, width=width, state="readonly")
|
||||
control.grid(row=1, column=0, sticky="ew")
|
||||
return {"frame": frame, "var": var, "combobox": control}
|
||||
|
||||
|
||||
def action_bar(parent, row: int, columnspan: int = 4):
|
||||
frame = ttk.Frame(parent, style="Panel.TFrame")
|
||||
frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=18, pady=(0, 16))
|
||||
return frame
|
||||
|
||||
|
||||
def button(parent, text: str, command: Callable, style: str = "Secondary.TButton"):
|
||||
btn = ttk.Button(parent, text=text, command=command, style=style)
|
||||
btn.pack(side="left", padx=(0, 10))
|
||||
return btn
|
||||
|
||||
|
||||
def set_entry_state(item, enabled: bool) -> None:
|
||||
item["entry"].configure(state="normal" if enabled else "disabled")
|
||||
+225
-135
@@ -1,159 +1,249 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext
|
||||
import threading
|
||||
import re
|
||||
from tkinter import messagebox
|
||||
|
||||
import logging
|
||||
from core.ui.basic_ui import BasicUI
|
||||
from core.Function.network_fun import NetworkManager
|
||||
from core.Function.network_fun import NetworkManager
|
||||
from core.ui.components import Console, Page, action_bar, button, combo, field, set_entry_state
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NetworkTab(ttk.Frame, BasicUI):
|
||||
"""网卡配置 Tab(兼容最新 NetworkManager)"""
|
||||
|
||||
class NetworkTab(Page):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.networkname_list = []
|
||||
self.networkconfig = []
|
||||
self.build_ui()
|
||||
self.netmgr = NetworkManager(self.result_box)
|
||||
super().__init__(parent, "网卡配置", "查看本机网卡信息,切换 DHCP,或写入静态 IPv4 / DNS 配置。")
|
||||
self.body.rowconfigure(4, weight=1)
|
||||
self.adapters = []
|
||||
self.profiles = {}
|
||||
|
||||
def build_ui(self):
|
||||
self.create_iface_section()
|
||||
self.create_config_section()
|
||||
self.create_action_section()
|
||||
self.create_output_section()
|
||||
selector = self.section("网卡选择", 0, columns=4)
|
||||
self.iface = combo(selector, "选择网卡", 1, 0, [], "", 44, colspan=2)
|
||||
self.iface["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.load_selected_adapter())
|
||||
self.status = field(selector, "连接状态", 1, 2, "", 14)
|
||||
self.status["entry"].configure(state="disabled")
|
||||
actions = action_bar(selector, 2, 4)
|
||||
self.refresh_btn = button(actions, "刷新网卡", self.refresh_adapters, "Primary.TButton")
|
||||
|
||||
def refresh_allnetwork(self):
|
||||
'''刷新网卡列表'''
|
||||
self.networkname_list = []
|
||||
try:
|
||||
self.networkconfigs = self.netmgr.get_network_info()
|
||||
for config in self.networkconfigs:
|
||||
self.networkname_list.append(config.get("name", "未知网卡"))
|
||||
self.iface_cb['combobox']['values'] = self.networkname_list
|
||||
if self.iface_cb['var'].get() == "" :
|
||||
self.iface_cb['var'].set(self.networkname_list[0])
|
||||
self.result_box.insert(tk.END, f"获取网卡信息完成,共:{len(self.networkname_list)}个启用网卡\n")
|
||||
self.refresh_network_callback()
|
||||
identity = self.section("网卡信息", 1, columns=4)
|
||||
self.description = field(identity, "设备描述", 1, 0, "", 46, colspan=2)
|
||||
self.mac = field(identity, "MAC 地址", 1, 2, "", 24)
|
||||
for item in (self.description, self.mac):
|
||||
item["entry"].configure(state="disabled")
|
||||
|
||||
logger.info(f"获取网卡信息完成,共:{len(self.networkname_list)}个启用网卡")
|
||||
logger.info(f"所有获取网卡信息:{self.networkconfigs}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"获取网卡信息失败: {e}")
|
||||
logger.error(f"获取网卡信息失败: {e}")
|
||||
self.networkconfigs = []
|
||||
config = self.section("IPv4 配置", 2, columns=4)
|
||||
self.dhcp = combo(config, "地址模式", 1, 0, ["DHCP 自动获取", "静态手动配置"], "DHCP 自动获取", 18)
|
||||
self.dhcp["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.update_entry_state())
|
||||
self.ipv4 = field(config, "IPv4 地址", 1, 1, "", 18)
|
||||
self.netmask = field(config, "子网掩码", 1, 2, "", 18)
|
||||
self.gateway = field(config, "默认网关", 1, 3, "", 18)
|
||||
self.dns1 = field(config, "首选 DNS", 2, 1, "", 18)
|
||||
self.dns2 = field(config, "备用 DNS", 2, 2, "", 18)
|
||||
config_actions = action_bar(config, 3, 4)
|
||||
self.apply_btn = button(config_actions, "应用配置", self.apply_settings, "Primary.TButton")
|
||||
self.reload_btn = button(config_actions, "重新读取", self.refresh_adapters, "Secondary.TButton")
|
||||
|
||||
# ---------------- UI 构建 ----------------
|
||||
def create_iface_section(self):
|
||||
frame = ttk.LabelFrame(self, text="网卡选择", padding=8)
|
||||
frame.pack(side='top', fill='x', padx=10, pady=6)
|
||||
profiles = self.section("配置模板", 3, columns=4)
|
||||
self.profile_name = field(profiles, "模板名称", 1, 0, "", 20)
|
||||
self.profile_select = combo(profiles, "选择模板", 1, 1, [], "", 24)
|
||||
profile_actions = action_bar(profiles, 2, 4)
|
||||
self.save_profile_btn = button(profile_actions, "保存当前为模板", self.save_profile, "Primary.TButton")
|
||||
self.apply_profile_btn = button(profile_actions, "套用模板到表单", self.apply_profile_to_form, "Secondary.TButton")
|
||||
self.delete_profile_btn = button(profile_actions, "删除模板", self.delete_profile, "Danger.TButton")
|
||||
|
||||
self.iface_cb = self.add_combobox(frame, "选择网卡", row=0, col=0, listbox=[], width=42, inivar=-1)
|
||||
self.iface_cb['combobox'].bind("<<ComboboxSelected>>", lambda e: self.refresh_network_callback())
|
||||
self.refresh_btn = self.add_button(frame, "刷新网卡列表", row=0, col=1, width=15, command=self.refresh_allnetwork)
|
||||
self.description_entry = self.add_input(frame, "网卡名称", row=1, col=0, inivar="", entry_width=45)
|
||||
self.description_entry['entry'].config(state='disabled')
|
||||
self.mac_entry = self.add_input(frame, "MAC地址", row=1, col=1, inivar="", entry_width=35)
|
||||
self.mac_entry['entry'].config(state='disabled')
|
||||
output = self.section("输出控制台", 4, columns=1)
|
||||
output.rowconfigure(1, weight=1)
|
||||
output.columnconfigure(0, weight=1)
|
||||
self.console = Console(output, height=12)
|
||||
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||
|
||||
def create_config_section(self):
|
||||
frame = ttk.LabelFrame(self, text="IP 配置(编辑后点击应用)", padding=8)
|
||||
frame.pack(side='top', fill='x', padx=10, pady=6)
|
||||
self.netmgr = NetworkManager(self.write)
|
||||
self.load_profiles()
|
||||
self.after(250, self.refresh_adapters)
|
||||
|
||||
self.ip_entry = self.add_input(frame, "IPv4 地址", row=0, col=0, inivar="", entry_width=20)
|
||||
self.mask_entry = self.add_input(frame, "子网掩码", row=0, col=1, inivar="", entry_width=20)
|
||||
self.gw_entry = self.add_input(frame, "默认网关", row=0, col=2, inivar="", entry_width=20)
|
||||
#self.dns1_entry = self.add_input(frame, "主 DNS", row=1, col=0, inivar="", entry_width=20)
|
||||
#self.dns2_entry = self.add_input(frame, "备选 DNS", row=1, col=1, inivar="", entry_width=20)
|
||||
self.dhcp_cb = self.add_combobox(frame, "自动获取", row=2, col=0, listbox=["是","否"], width=5, inivar=-1)
|
||||
self.dhcp_cb['combobox'].bind("<<ComboboxSelected>>", lambda e: self.dhcp_entry_state())
|
||||
def write(self, text, tag=None):
|
||||
self.console.write(text, tag)
|
||||
|
||||
def create_action_section(self):
|
||||
frame = ttk.LabelFrame(self, text="修改操作", padding=8)
|
||||
frame.pack(side='top', fill='x', padx=10, pady=6)
|
||||
def refresh_adapters(self, clear=True):
|
||||
self.refresh_btn.configure(state="disabled")
|
||||
self.reload_btn.configure(state="disabled")
|
||||
if clear:
|
||||
self.console.clear()
|
||||
self.write("正在读取本机网卡信息...\n", "muted")
|
||||
|
||||
self.apply_btn = self.add_button(frame, "应用修改", row=0, col=0, width=10, command=self.apply_btn_callback)
|
||||
self.refresh_info_btn = self.add_button(frame, "刷新当前信息", row=0, col=2, width=15, command=self.refresh_allnetwork)
|
||||
def worker():
|
||||
try:
|
||||
adapters = self.netmgr.get_network_info()
|
||||
self.after(0, lambda: self.on_adapters_loaded(adapters))
|
||||
except Exception as exc:
|
||||
self.after(0, lambda: self.on_refresh_failed(exc))
|
||||
|
||||
def create_output_section(self):
|
||||
frame = ttk.LabelFrame(self, text="输出信息", padding=6)
|
||||
frame.pack(side='top', fill='both', expand=True, padx=10, pady=6)
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
self.result_box = scrolledtext.ScrolledText(frame, width=100, height=12)
|
||||
self.result_box.pack(fill='both', expand=True, padx=4, pady=4)
|
||||
|
||||
|
||||
# ---------------- 事件回调 ----------------
|
||||
def refresh_network_callback(self):
|
||||
'''刷新当前网卡信息'''
|
||||
# 刷新所有所有网卡信息
|
||||
#self.refresh_allnetwork()
|
||||
# 获取选中的网卡信息
|
||||
iface_name = self.iface_cb['var'].get()
|
||||
config = self.get_network_config(iface_name)
|
||||
# 不可修改配置
|
||||
self.description_entry['var'].set(config.get("description", ""))
|
||||
self.mac_entry['var'].set(config.get("mac", ""))
|
||||
# 可修改配置
|
||||
self.ip_entry['var'].set(config.get("ipv4", ""))
|
||||
self.mask_entry['var'].set(config.get("netmask", ""))
|
||||
self.gw_entry['var'].set(config.get("gateway", ""))
|
||||
#self.dns1_entry['var'].set(config.get("dns1", ""))
|
||||
#self.dns2_entry['var'].set(config.get("dns2", ""))
|
||||
if config.get("dhcp_enabled", "") == "是":
|
||||
self.dhcp_cb['var'].set("是")
|
||||
elif config.get("dhcp_enabled", "") == "否":
|
||||
self.dhcp_cb['var'].set("否")
|
||||
def on_adapters_loaded(self, adapters):
|
||||
self.adapters = adapters
|
||||
names = [adapter["name"] for adapter in adapters]
|
||||
self.iface["combobox"]["values"] = names
|
||||
if names:
|
||||
current = self.iface["var"].get()
|
||||
self.iface["var"].set(current if current in names else names[0])
|
||||
self.load_selected_adapter()
|
||||
self.write(f"读取完成,共发现 {len(names)} 个网卡\n", "success")
|
||||
else:
|
||||
self.dhcp_cb['var'].set("")
|
||||
self.dhcp_entry_state()
|
||||
self.get_network_settings()
|
||||
self.output_network_settings()
|
||||
self.write("未发现可用网卡\n", "warning")
|
||||
self.refresh_btn.configure(state="normal")
|
||||
self.reload_btn.configure(state="normal")
|
||||
|
||||
def get_network_config(self, iface_name):
|
||||
'''根据网卡名称获取对应的配置信息'''
|
||||
for config in self.networkconfigs:
|
||||
if config.get("name") == iface_name:
|
||||
return config
|
||||
def on_refresh_failed(self, exc):
|
||||
self.write(f"读取网卡失败: {exc}\n", "error")
|
||||
self.refresh_btn.configure(state="normal")
|
||||
self.reload_btn.configure(state="normal")
|
||||
messagebox.showerror("读取网卡失败", str(exc))
|
||||
|
||||
def load_selected_adapter(self):
|
||||
adapter = self.current_adapter()
|
||||
if not adapter:
|
||||
return
|
||||
|
||||
self.status["entry"].configure(state="normal")
|
||||
self.description["entry"].configure(state="normal")
|
||||
self.mac["entry"].configure(state="normal")
|
||||
|
||||
self.status["var"].set(adapter.get("status", ""))
|
||||
self.description["var"].set(adapter.get("description", ""))
|
||||
self.mac["var"].set(adapter.get("mac", ""))
|
||||
self.ipv4["var"].set(adapter.get("ipv4", ""))
|
||||
self.netmask["var"].set(adapter.get("netmask", ""))
|
||||
self.gateway["var"].set(adapter.get("gateway", ""))
|
||||
self.dns1["var"].set(adapter.get("dns1", ""))
|
||||
self.dns2["var"].set(adapter.get("dns2", ""))
|
||||
self.dhcp["var"].set("DHCP 自动获取" if adapter.get("dhcp_enabled") else "静态手动配置")
|
||||
|
||||
self.status["entry"].configure(state="disabled")
|
||||
self.description["entry"].configure(state="disabled")
|
||||
self.mac["entry"].configure(state="disabled")
|
||||
self.update_entry_state()
|
||||
self.write_current_adapter(adapter)
|
||||
|
||||
def current_adapter(self):
|
||||
name = self.iface["var"].get()
|
||||
for adapter in self.adapters:
|
||||
if adapter.get("name") == name:
|
||||
return adapter
|
||||
return None
|
||||
|
||||
def dhcp_entry_state(self):
|
||||
'''根据 DHCP 选择框设置输入框状态'''
|
||||
if self.dhcp_cb['var'].get() == "是":
|
||||
# 禁用手动输入
|
||||
self.ip_entry['entry'].config(state='disabled')
|
||||
self.mask_entry['entry'].config(state='disabled')
|
||||
self.gw_entry['entry'].config(state='disabled')
|
||||
else:
|
||||
# 启用手动输入
|
||||
self.ip_entry['entry'].config(state='normal')
|
||||
self.mask_entry['entry'].config(state='normal')
|
||||
self.gw_entry['entry'].config(state='normal')
|
||||
def update_entry_state(self):
|
||||
static = self.dhcp["var"].get() == "静态手动配置"
|
||||
for item in (self.ipv4, self.netmask, self.gateway, self.dns1, self.dns2):
|
||||
set_entry_state(item, static)
|
||||
|
||||
def get_network_settings(self):
|
||||
'''获取当前输入的网卡配置信息'''
|
||||
self.networkconfig = {
|
||||
"name": re.sub(r"^(.*?(适配器))\s*", "", self.iface_cb['var'].get()).strip(),
|
||||
"ipv4": self.ip_entry['var'].get(),
|
||||
"netmask": self.mask_entry['var'].get(),
|
||||
"gateway": self.gw_entry['var'].get(),
|
||||
#"dns1": self.dns1_entry['var'].get(),
|
||||
#"dns2": self.dns2_entry['var'].get(),
|
||||
"dhcp_enabled": self.dhcp_cb['var'].get() == "是"
|
||||
def write_current_adapter(self, adapter):
|
||||
self.write("\n当前网卡:\n", "muted")
|
||||
rows = [
|
||||
("名称", adapter.get("name", "")),
|
||||
("描述", adapter.get("description", "")),
|
||||
("状态", adapter.get("status", "")),
|
||||
("速率", adapter.get("link_speed", "")),
|
||||
("接口索引", adapter.get("interface_index", "")),
|
||||
("IPv4", adapter.get("ipv4", "")),
|
||||
("IPv6", ", ".join(adapter.get("ipv6", []))),
|
||||
("掩码", adapter.get("netmask", "")),
|
||||
("网关", adapter.get("gateway", "")),
|
||||
("DNS", ", ".join(value for value in (adapter.get("dns1", ""), adapter.get("dns2", "")) if value)),
|
||||
("DHCP", "是" if adapter.get("dhcp_enabled") else "否"),
|
||||
("DHCP 服务器", adapter.get("dhcp_server", "")),
|
||||
("租约获取", adapter.get("dhcp_lease_obtained", "")),
|
||||
("租约过期", adapter.get("dhcp_lease_expires", "")),
|
||||
]
|
||||
for key, value in rows:
|
||||
self.write(f" {key}: {value}\n", "muted")
|
||||
|
||||
def collect_settings(self):
|
||||
return {
|
||||
"name": self.iface["var"].get(),
|
||||
"dhcp_enabled": self.dhcp["var"].get() == "DHCP 自动获取",
|
||||
"ipv4": self.ipv4["var"].get(),
|
||||
"netmask": self.netmask["var"].get(),
|
||||
"gateway": self.gateway["var"].get(),
|
||||
"dns1": self.dns1["var"].get(),
|
||||
"dns2": self.dns2["var"].get(),
|
||||
}
|
||||
|
||||
def output_network_settings(self):
|
||||
'''输出当前网卡配置信息到日志框'''
|
||||
self.result_box.insert(tk.END, f"当前网卡配置:\n")
|
||||
for key, value in self.networkconfig.items():
|
||||
self.result_box.insert(tk.END, f" {key}: {value}\n")
|
||||
self.result_box.see(tk.END)
|
||||
def apply_settings(self):
|
||||
settings = self.collect_settings()
|
||||
if not settings["name"]:
|
||||
messagebox.showwarning("无法应用配置", "请先选择网卡")
|
||||
return
|
||||
|
||||
def apply_btn_callback(self):
|
||||
'''应用当前网卡配置信息'''
|
||||
self.get_network_settings()
|
||||
self.netmgr.set_network_info(self.networkconfig)
|
||||
#self.refresh_allnetwork()
|
||||
self.apply_btn.configure(state="disabled")
|
||||
self.write("\n开始应用配置。修改网卡通常需要管理员权限。\n", "warning")
|
||||
|
||||
def worker():
|
||||
try:
|
||||
self.netmgr.set_network_info(settings)
|
||||
self.after(0, self.on_apply_success)
|
||||
except Exception as exc:
|
||||
self.after(0, lambda: self.on_apply_failed(exc))
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def on_apply_success(self):
|
||||
self.apply_btn.configure(state="normal")
|
||||
self.write("配置应用完成,正在刷新网卡信息...\n", "success")
|
||||
self.refresh_adapters(clear=False)
|
||||
|
||||
def on_apply_failed(self, exc):
|
||||
self.apply_btn.configure(state="normal")
|
||||
self.write(f"配置应用失败: {exc}\n", "error")
|
||||
messagebox.showerror("配置应用失败", f"{exc}\n\n请确认程序已用管理员权限运行。")
|
||||
|
||||
def load_profiles(self):
|
||||
try:
|
||||
self.profiles = self.netmgr.load_profiles()
|
||||
except Exception as exc:
|
||||
self.profiles = {}
|
||||
self.write(f"读取模板失败: {exc}\n", "warning")
|
||||
names = sorted(self.profiles.keys())
|
||||
self.profile_select["combobox"]["values"] = names
|
||||
if names and not self.profile_select["var"].get():
|
||||
self.profile_select["var"].set(names[0])
|
||||
|
||||
def save_profile(self):
|
||||
try:
|
||||
name = self.profile_name["var"].get().strip()
|
||||
if not name:
|
||||
adapter = self.current_adapter()
|
||||
name = adapter.get("name", "未命名模板") if adapter else "未命名模板"
|
||||
self.netmgr.save_profile(name, self.collect_settings())
|
||||
self.profile_name["var"].set(name)
|
||||
self.profile_select["var"].set(name)
|
||||
self.load_profiles()
|
||||
self.write(f"已保存配置模板: {name}\n", "success")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("保存模板失败", str(exc))
|
||||
|
||||
def apply_profile_to_form(self):
|
||||
name = self.profile_select["var"].get()
|
||||
profile = self.profiles.get(name)
|
||||
if not profile:
|
||||
messagebox.showinfo("提示", "请选择要套用的模板")
|
||||
return
|
||||
|
||||
self.dhcp["var"].set("DHCP 自动获取" if profile.get("dhcp_enabled") else "静态手动配置")
|
||||
self.ipv4["var"].set(profile.get("ipv4", ""))
|
||||
self.netmask["var"].set(profile.get("netmask", ""))
|
||||
self.gateway["var"].set(profile.get("gateway", ""))
|
||||
self.dns1["var"].set(profile.get("dns1", ""))
|
||||
self.dns2["var"].set(profile.get("dns2", ""))
|
||||
self.update_entry_state()
|
||||
self.write(f"已套用模板到表单: {name}\n", "success")
|
||||
|
||||
def delete_profile(self):
|
||||
name = self.profile_select["var"].get()
|
||||
if not name:
|
||||
messagebox.showinfo("提示", "请选择要删除的模板")
|
||||
return
|
||||
if not messagebox.askyesno("确认删除模板", f"确定删除配置模板“{name}”吗?"):
|
||||
return
|
||||
try:
|
||||
self.netmgr.delete_profile(name)
|
||||
self.profile_select["var"].set("")
|
||||
self.load_profiles()
|
||||
self.write(f"已删除配置模板: {name}\n", "success")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("删除模板失败", str(exc))
|
||||
|
||||
+213
-101
@@ -1,122 +1,234 @@
|
||||
import re
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from core.ui.basic_ui import BasicUI
|
||||
from core.Function.network_fun import NetworkManager
|
||||
from core.Function.ping_fun import PingFun
|
||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PingTab(ttk.Frame, BasicUI):
|
||||
DEFAULT_SOURCE = "默认路由"
|
||||
IP_PATTERN = re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)")
|
||||
|
||||
|
||||
class PingTab(Page):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.ping_ui()
|
||||
self.ping_fun = PingFun(self.result_box)
|
||||
super().__init__(parent, "Ping 探测", "单点 Ping、批量探活、参数化诊断与结果导出。")
|
||||
self.body.rowconfigure(2, weight=1)
|
||||
|
||||
def ping_ui(self):
|
||||
"""ping界面布局"""
|
||||
self.create_assignIP_section()
|
||||
self.create_batchIP_section()
|
||||
self.create_outputping_section()
|
||||
# --------------------------------------UI界面布局函数--------------------------------------
|
||||
def create_assignIP_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="指定地址 Ping 目标地址")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
status = self.section("实时状态", 0, columns=7)
|
||||
self.state = field(status, "状态", 1, 0, "等待", 12)
|
||||
self.sent = field(status, "发送", 1, 1, "0", 8)
|
||||
self.received = field(status, "接收", 1, 2, "0", 8)
|
||||
self.loss = field(status, "丢包率", 1, 3, "0.0%", 10)
|
||||
self.avg = field(status, "平均延迟", 1, 4, "0.0 ms", 12)
|
||||
self.jitter = field(status, "抖动", 1, 5, "0.0 ms", 12)
|
||||
self.quality = field(status, "质量", 1, 6, "等待", 10)
|
||||
for item in (self.state, self.sent, self.received, self.loss, self.avg, self.jitter, self.quality):
|
||||
item["entry"].configure(state="disabled")
|
||||
|
||||
self.entry_assignIP_A = self.add_input(frame, "本地IP", row=0, col=0)
|
||||
self.entry_assignIP_B = self.add_input(frame, "目标IP", row=0, col=1, inivar="127.0.0.1")
|
||||
self.assignIP_startPing = self.add_button(frame, "开始", row=0, col=2, command=self.btn_assignIP_startPing)
|
||||
self.assignIP_stopPing = self.add_button(frame, "停止", row=0, col=3, command=self.btn_assignIP_stopPing)
|
||||
modes = self.section("探测模式", 1, columns=1)
|
||||
self.tabs = ttk.Notebook(modes)
|
||||
self.tabs.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 18))
|
||||
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||
self.tabs.add(self.single_tab, text="单点探测")
|
||||
self.tabs.add(self.batch_tab, text="批量探活")
|
||||
self.build_single_tab()
|
||||
self.build_batch_tab()
|
||||
|
||||
def create_batchIP_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="指定地址 Ping 批量地址")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
output = self.section("输出控制台", 2, columns=1)
|
||||
output.rowconfigure(1, weight=1)
|
||||
output.columnconfigure(0, weight=1)
|
||||
self.console = Console(output, height=16)
|
||||
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||
|
||||
self.entry_batchIP_A = self.add_input(frame, "本地IP", row=0, col=0)
|
||||
self.entry_batchIP_B = self.add_input(frame, "C类网段", row=0, col=1, inivar="192.168.1.", entry_width=15)
|
||||
self.entry_batchIP_B_begin = self.add_input(frame, "起始地址", row=0, col=2, inivar="1", entry_width=5)
|
||||
self.entry_batchIP_B_end = self.add_input(frame, "结束地址", row=0, col=3, inivar="255", entry_width=5)
|
||||
self.batchIP_startPing = self.add_button(frame, "开始", row=0, col=4, command=self.btn_batchIP_startPing)
|
||||
self.batchIP_stopPing = self.add_button(frame, "停止", row=0, col=5, command=self.btn_batchIP_stopPing)
|
||||
self.ping_fun = PingFun(self.write, self.on_task_done, self.update_status)
|
||||
self.source_loader = NetworkManager(lambda _text, _tag=None: None)
|
||||
self.after(350, self.load_source_ips)
|
||||
|
||||
def create_outputping_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="PING 结果输出")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
def build_single_tab(self):
|
||||
for col in range(6):
|
||||
self.single_tab.columnconfigure(col, weight=1)
|
||||
|
||||
self.result_box = scrolledtext.ScrolledText(frame, width=100, height=20)
|
||||
self.result_box.pack(pady=10)
|
||||
# --------------------------------------按钮回调函数--------------------------------------
|
||||
def btn_assignIP_startPing(self):
|
||||
if not self.entry_assignIP_B['var'].get():
|
||||
messagebox.showwarning("输入错误", "请输入目标IP或域名!")
|
||||
return
|
||||
logger.info(f"开始由{self.entry_assignIP_A['var'].get()} Ping {self.entry_assignIP_B['var'].get()}")
|
||||
self.ping_fun.strat_ping(self.entry_assignIP_B['var'].get(),local_ip=self.entry_assignIP_A['var'].get(),
|
||||
callback=self.assignIP_ping_callback)
|
||||
self.assignIP_startPing['btn'].config(state='disabled')
|
||||
self.local_ip = combo(self.single_tab, "本地源 IP", 0, 0, [], "", 22)
|
||||
self.target = field(self.single_tab, "目标 IP / 域名", 0, 1, "127.0.0.1", 28)
|
||||
self.single_mode = combo(self.single_tab, "模式", 0, 2, ["持续", "指定次数"], "持续", 12)
|
||||
self.count = field(self.single_tab, "次数", 0, 3, "4", 8)
|
||||
self.interval = field(self.single_tab, "间隔 ms", 0, 4, "1000", 10)
|
||||
self.timeout = field(self.single_tab, "超时 ms", 0, 5, "1200", 10)
|
||||
|
||||
def btn_assignIP_stopPing(self):
|
||||
logger.info(f"停止由{self.entry_assignIP_A['var'].get()} Ping {self.entry_assignIP_B['var'].get()}")
|
||||
self.size = field(self.single_tab, "包大小 bytes", 1, 0, "32", 12)
|
||||
self.ttl = field(self.single_tab, "TTL(0=默认)", 1, 1, "0", 12)
|
||||
self.df_var = tk.BooleanVar(value=False)
|
||||
df_frame = ttk.Frame(self.single_tab, style="Panel.TFrame")
|
||||
df_frame.grid(row=1, column=2, sticky="ew", padx=18, pady=(22, 14))
|
||||
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.df_var).pack(anchor="w")
|
||||
|
||||
actions = action_bar(self.single_tab, 2, 6)
|
||||
self.start_btn = button(actions, "开始 Ping", self.start_ping, "Primary.TButton")
|
||||
self.stop_btn = button(actions, "停止", self.stop_ping, "Danger.TButton")
|
||||
self.reload_sources_btn = button(actions, "刷新源 IP", self.load_source_ips, "Secondary.TButton")
|
||||
|
||||
def build_batch_tab(self):
|
||||
for col in range(6):
|
||||
self.batch_tab.columnconfigure(col, weight=1)
|
||||
|
||||
self.batch_local_ip = combo(self.batch_tab, "本地源 IP", 0, 0, [], "", 22)
|
||||
self.targets = field(self.batch_tab, "目标范围 / 列表", 0, 1, "192.168.1.0/24", 42, colspan=2)
|
||||
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 3, "1200", 10)
|
||||
self.batch_size = field(self.batch_tab, "包大小 bytes", 0, 4, "32", 12)
|
||||
self.workers = field(self.batch_tab, "并发数", 0, 5, "64", 10)
|
||||
|
||||
self.batch_ttl = field(self.batch_tab, "TTL(0=默认)", 1, 0, "0", 12)
|
||||
self.batch_df_var = tk.BooleanVar(value=False)
|
||||
df_frame = ttk.Frame(self.batch_tab, style="Panel.TFrame")
|
||||
df_frame.grid(row=1, column=1, sticky="ew", padx=18, pady=(22, 14))
|
||||
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.batch_df_var).pack(anchor="w")
|
||||
|
||||
actions = action_bar(self.batch_tab, 2, 6)
|
||||
self.batch_start_btn = button(actions, "批量 Ping", self.start_batch_ping, "Primary.TButton")
|
||||
self.batch_stop_btn = button(actions, "停止批量", self.stop_batch_ping, "Danger.TButton")
|
||||
self.import_btn = button(actions, "导入目标", self.import_targets, "Secondary.TButton")
|
||||
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||
self.reload_batch_sources_btn = button(actions, "刷新源 IP", self.load_source_ips, "Secondary.TButton")
|
||||
|
||||
def write(self, text, tag=None):
|
||||
self.console.write(text, tag)
|
||||
|
||||
def clear(self):
|
||||
self.console.clear()
|
||||
self.update_status({"state": "等待", "sent": 0, "received": 0, "lost": 0, "loss_rate": 0, "avg_rtt": 0, "jitter": 0, "quality": "等待"})
|
||||
|
||||
def start_ping(self):
|
||||
try:
|
||||
self.clear()
|
||||
self.ping_fun.start_ping(self.target["var"].get(), self.selected_source_ip(self.local_ip), self.single_options())
|
||||
self._set_start_buttons("disabled")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("无法开始 Ping", str(exc))
|
||||
|
||||
def stop_ping(self):
|
||||
self.ping_fun.stop_ping()
|
||||
self.assignIP_startPing['btn'].config(state='normal')
|
||||
|
||||
def assignIP_ping_callback(self):
|
||||
self.assignIP_startPing['btn'].config(state='normal')
|
||||
def start_batch_ping(self):
|
||||
try:
|
||||
self.clear()
|
||||
self.ping_fun.start_batch_ping(self.targets["var"].get(), self.selected_source_ip(self.batch_local_ip), self.batch_options())
|
||||
self._set_start_buttons("disabled")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("无法开始批量 Ping", str(exc))
|
||||
|
||||
def btn_batchIP_startPing(self):
|
||||
net_prefix = self.entry_batchIP_B['var'].get()
|
||||
start = self.entry_batchIP_B_begin['var'].get()
|
||||
end = self.entry_batchIP_B_end['var'].get()
|
||||
local_ip = self.entry_batchIP_A['var'].get()
|
||||
# ========= 输入参数检查 =========
|
||||
if not net_prefix or start is None or end is None:
|
||||
messagebox.showwarning("输入错误", "请输入完整的网段、起始地址和结束地址!")
|
||||
def stop_batch_ping(self):
|
||||
self.ping_fun.stop_batch_ping()
|
||||
|
||||
def single_options(self):
|
||||
return {
|
||||
"mode": self.single_mode["var"].get(),
|
||||
"count": self.count["var"].get(),
|
||||
"interval_ms": self.interval["var"].get(),
|
||||
"timeout_ms": self.timeout["var"].get(),
|
||||
"size": self.size["var"].get(),
|
||||
"ttl": self.ttl["var"].get(),
|
||||
"dont_fragment": self.df_var.get(),
|
||||
}
|
||||
|
||||
def batch_options(self):
|
||||
return {
|
||||
"mode": "指定次数",
|
||||
"count": 1,
|
||||
"interval_ms": 1000,
|
||||
"timeout_ms": self.batch_timeout["var"].get(),
|
||||
"size": self.batch_size["var"].get(),
|
||||
"ttl": self.batch_ttl["var"].get(),
|
||||
"dont_fragment": self.batch_df_var.get(),
|
||||
"workers": self.workers["var"].get(),
|
||||
}
|
||||
|
||||
def update_status(self, stats):
|
||||
def apply():
|
||||
values = [
|
||||
(self.state, stats.get("state", "等待")),
|
||||
(self.sent, str(stats.get("sent", 0))),
|
||||
(self.received, str(stats.get("received", 0))),
|
||||
(self.loss, f"{stats.get('loss_rate', 0):.1f}%"),
|
||||
(self.avg, f"{stats.get('avg_rtt', 0):.1f} ms"),
|
||||
(self.jitter, f"{stats.get('jitter', 0):.1f} ms"),
|
||||
(self.quality, stats.get("quality", "等待")),
|
||||
]
|
||||
for item, value in values:
|
||||
item["entry"].configure(state="normal")
|
||||
item["var"].set(value)
|
||||
item["entry"].configure(state="disabled")
|
||||
|
||||
self.after(0, apply)
|
||||
|
||||
def load_source_ips(self):
|
||||
def worker():
|
||||
values = [DEFAULT_SOURCE]
|
||||
try:
|
||||
for adapter in self.source_loader.get_network_info():
|
||||
ip = adapter.get("ipv4", "")
|
||||
if ip:
|
||||
name = adapter.get("name", "").strip()
|
||||
values.append(f"{name} - {ip}" if name else ip)
|
||||
except Exception as exc:
|
||||
self.write(f"读取本地源 IP 失败: {exc}\n", "warning")
|
||||
self.after(0, lambda: self.apply_source_ips(values))
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def apply_source_ips(self, values):
|
||||
values = list(dict.fromkeys(values))
|
||||
self.local_ip["combobox"]["values"] = values
|
||||
self.batch_local_ip["combobox"]["values"] = values
|
||||
if self.local_ip["var"].get() not in values:
|
||||
self.local_ip["var"].set(values[0])
|
||||
if self.batch_local_ip["var"].get() not in values:
|
||||
self.batch_local_ip["var"].set(values[0])
|
||||
|
||||
def selected_source_ip(self, item):
|
||||
value = item["var"].get().strip()
|
||||
if not value or value == DEFAULT_SOURCE:
|
||||
return ""
|
||||
match = IP_PATTERN.search(value)
|
||||
return match.group(0) if match else value
|
||||
|
||||
def import_targets(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title="导入 Ping 目标",
|
||||
filetypes=[("文本文件", "*.txt *.csv"), ("所有文件", "*.*")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
except ValueError:
|
||||
messagebox.showwarning("输入错误", "起始地址和结束地址必须是整数!")
|
||||
with open(path, "r", encoding="utf-8-sig") as file:
|
||||
items = []
|
||||
for line in file:
|
||||
items.extend(part.strip() for part in line.replace(",", ",").split(",") if part.strip())
|
||||
self.targets["var"].set(",".join(items))
|
||||
self.write(f"已导入 {len(items)} 个目标/表达式\n", "success")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("导入失败", str(exc))
|
||||
|
||||
def export_results(self):
|
||||
path = filedialog.asksaveasfilename(
|
||||
title="导出批量 Ping 结果",
|
||||
defaultextension=".csv",
|
||||
filetypes=[("CSV 文件", "*.csv")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
if start < 1 or end > 255:
|
||||
messagebox.showwarning("输入错误", "起始地址最小为1,结束地址最大为255!")
|
||||
return
|
||||
if start > end:
|
||||
messagebox.showwarning("输入错误", "起始地址不能大于结束地址!")
|
||||
return
|
||||
if not net_prefix.endswith('.'):
|
||||
messagebox.showwarning("输入错误", "网段必须以 '.' 结尾,例如:192.168.1.")
|
||||
return
|
||||
if local_ip == "":
|
||||
logger.info(f"开始批量 Ping {net_prefix}{start} - {net_prefix}{end}")
|
||||
else:
|
||||
logger.info(f"开始由{local_ip} 批量 Ping {net_prefix}{start} - {net_prefix}{end}")
|
||||
|
||||
self.ping_fun.start_batch_ping(net_prefix, start, end, local_ip=local_ip, callback=self.batchIP_ping_callback)
|
||||
self.batchIP_startPing['btn'].config(state='disabled')
|
||||
try:
|
||||
self.ping_fun.export_batch_results(path)
|
||||
self.write(f"已导出结果: {path}\n", "success")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("导出失败", str(exc))
|
||||
|
||||
def btn_batchIP_stopPing(self):
|
||||
net_prefix = self.entry_batchIP_B['var'].get()
|
||||
start = self.entry_batchIP_B_begin['var'].get()
|
||||
end = self.entry_batchIP_B_end['var'].get()
|
||||
local_ip = self.entry_batchIP_A['var'].get()
|
||||
if local_ip == "":
|
||||
logger.info(f"停止批量 Ping {net_prefix}{start} - {net_prefix}{end}")
|
||||
else:
|
||||
logger.info(f"停止由{local_ip} 批量 Ping {net_prefix}{start} - {net_prefix}{end}")
|
||||
self.ping_fun.stop_batch_ping()
|
||||
self.batchIP_startPing['btn'].config(state='normal')
|
||||
def on_task_done(self):
|
||||
self.after(0, lambda: self._set_start_buttons("normal"))
|
||||
|
||||
def batchIP_ping_callback(self):
|
||||
self.batchIP_startPing['btn'].config(state='normal')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _set_start_buttons(self, state):
|
||||
self.start_btn.configure(state=state)
|
||||
self.batch_start_btn.configure(state=state)
|
||||
|
||||
+303
-110
@@ -1,135 +1,328 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
from core.ui.basic_ui import BasicUI
|
||||
from core.Function.telnet_fun import PortScanner
|
||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TelnetTab(ttk.Frame, BasicUI):
|
||||
PORT_PRESETS = {
|
||||
"常用端口": "21,22,23,25,53,80,110,139,143,443,445,1433,3306,3389,5432,6379,8080,8443,9200",
|
||||
"Web 服务": "80,443,8000-8010,8080,8081,8443,8888,9000",
|
||||
"数据库": "1433,1521,3306,5432,6379,9200,27017",
|
||||
"远程管理": "22,23,3389,5900,5985,5986",
|
||||
"邮件服务": "25,110,143,465,587,993,995",
|
||||
"全端口": "1-65535",
|
||||
}
|
||||
|
||||
|
||||
class TelnetTab(Page):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.telnet_ui()
|
||||
self.telnet_fun = PortScanner(self.result_box)
|
||||
super().__init__(parent, "端口扫描", "单端口测试、端口扫描、批量主机巡检、服务识别与结果导出。")
|
||||
self.body.rowconfigure(3, weight=1)
|
||||
|
||||
def telnet_ui(self):
|
||||
"""端口扫描界面布局"""
|
||||
self.create_assigntelnet_section()
|
||||
self.create_listtelnet_section()
|
||||
self.create_batchtelnet_section()
|
||||
self.create_outputping_section()
|
||||
# --------------------------------------UI界面布局函数--------------------------------------
|
||||
def create_assigntelnet_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="指定端口连接测试")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
status = self.section("实时状态", 0, columns=7)
|
||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||
self.total = field(status, "总数", 1, 1, "0", 8)
|
||||
self.scanned = field(status, "已扫", 1, 2, "0", 8)
|
||||
self.open_count = field(status, "开放", 1, 3, "0", 8)
|
||||
self.closed_count = field(status, "关闭", 1, 4, "0", 8)
|
||||
self.timeout_count = field(status, "超时", 1, 5, "0", 8)
|
||||
self.progress = field(status, "进度 / 耗时", 1, 6, "0.0% / 0.0s", 16)
|
||||
for item in (self.state, self.total, self.scanned, self.open_count, self.closed_count, self.timeout_count, self.progress):
|
||||
item["entry"].configure(state="disabled")
|
||||
|
||||
#self.entry_assignIP_A = self.add_input(frame, "本地IP", row=0, col=0)
|
||||
self.assignTelnet_IP = self.add_input(frame, "目标IP", row=0, col=0, inivar="202.89.233.100")
|
||||
self.assignTelnet_port = self.add_input(frame, "目标端口", row=0, col=1, inivar="443", entry_width=10)
|
||||
self.assignTelnet_test = self.add_button(frame, "测试", row=0, col=2, command=self.btn_assignTelnet_test)
|
||||
modes = self.section("扫描模式", 1, columns=1)
|
||||
self.tabs = ttk.Notebook(modes)
|
||||
self.tabs.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 18))
|
||||
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||
self.scan_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||
self.tabs.add(self.single_tab, text="单端口测试")
|
||||
self.tabs.add(self.scan_tab, text="端口扫描")
|
||||
self.tabs.add(self.batch_tab, text="批量主机扫描")
|
||||
self.build_single_tab()
|
||||
self.build_scan_tab()
|
||||
self.build_batch_tab()
|
||||
|
||||
def create_listtelnet_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="批量端口连接测试")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
results = self.section("结果列表", 2, columns=1)
|
||||
results.rowconfigure(1, weight=1)
|
||||
results.columnconfigure(0, weight=1)
|
||||
self.results_tree = ttk.Treeview(
|
||||
results,
|
||||
columns=("host", "resolved_ip", "port", "service", "status", "latency", "banner"),
|
||||
show="headings",
|
||||
height=8,
|
||||
)
|
||||
headings = {
|
||||
"host": "目标",
|
||||
"resolved_ip": "解析 IP",
|
||||
"port": "端口",
|
||||
"service": "服务",
|
||||
"status": "状态",
|
||||
"latency": "延迟",
|
||||
"banner": "Banner / 错误",
|
||||
}
|
||||
widths = {
|
||||
"host": 150,
|
||||
"resolved_ip": 130,
|
||||
"port": 70,
|
||||
"service": 100,
|
||||
"status": 80,
|
||||
"latency": 80,
|
||||
"banner": 320,
|
||||
}
|
||||
for column, title in headings.items():
|
||||
self.results_tree.heading(column, text=title)
|
||||
self.results_tree.column(column, width=widths[column], anchor="w")
|
||||
self.results_tree.tag_configure("open", foreground="#15803d")
|
||||
self.results_tree.tag_configure("timeout", foreground="#b7791f")
|
||||
self.results_tree.tag_configure("unreachable", foreground="#b7791f")
|
||||
self.results_tree.tag_configure("dns_error", foreground="#dc2626")
|
||||
self.results_tree.tag_configure("error", foreground="#dc2626")
|
||||
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||
result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview)
|
||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
||||
|
||||
self.listtelnet_IP = self.add_input(frame, "目标IP", row=0, col=0, inivar="202.89.233.100")
|
||||
self.listtelnet_ports = self.add_input(frame, "端口列表", row=0, col=1, inivar="21,22,23,25,80,110,143,443,1433,3306,3389", entry_width=40)
|
||||
self.listtelnet_start = self.add_button(frame, "开始", row=0, col=2, command=self.btn_listTelnet_start)
|
||||
self.listtelnet_stop = self.add_button(frame, "停止", row=0, col=3, command=self.btn_listTelnet_stop)
|
||||
|
||||
def create_batchtelnet_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="批量端口连接测试")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
output = self.section("输出控制台", 3, columns=1)
|
||||
output.rowconfigure(1, weight=1)
|
||||
output.columnconfigure(0, weight=1)
|
||||
self.console = Console(output, height=12)
|
||||
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||
|
||||
self.batchTelnet_IP = self.add_input(frame, "目标IP", row=0, col=0, inivar="202.89.233.100")
|
||||
self.batchTelnet_port_begin = self.add_input(frame, "起始端口", row=0, col=1, inivar="100", entry_width=10)
|
||||
self.batchTelnet_port_end = self.add_input(frame, "结束端口", row=0, col=2, inivar="1000", entry_width=10)
|
||||
self.batchTelnet_start = self.add_button(frame, "开始", row=0, col=3, command=self.btn_batchTelnet_start)
|
||||
self.batchTelnet_stop = self.add_button(frame, "停止", row=0, col=4, command=self.btn_batchTelnet_stop)
|
||||
self.scanner = PortScanner(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||
|
||||
def create_outputping_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="端口扫描结果输出")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
def build_single_tab(self):
|
||||
for col in range(5):
|
||||
self.single_tab.columnconfigure(col, weight=1)
|
||||
|
||||
self.result_box = scrolledtext.ScrolledText(frame, width=100, height=20)
|
||||
self.result_box.pack(pady=10)
|
||||
# --------------------------------------按钮回调函数--------------------------------------
|
||||
def btn_assignTelnet_test(self):
|
||||
self.IP = self.assignTelnet_IP['var'].get()
|
||||
self.port = self.assignTelnet_port['var'].get()
|
||||
if not self.IP:
|
||||
messagebox.showwarning("输入错误", "请输入目标IP或域名!")
|
||||
return
|
||||
if not self.port:
|
||||
messagebox.showwarning("输入错误", "请输入目标端口!")
|
||||
self.single_host = field(self.single_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 26)
|
||||
self.single_port = field(self.single_tab, "目标端口", 0, 1, "443", 12)
|
||||
self.single_timeout = field(self.single_tab, "超时 ms", 0, 2, "1000", 10)
|
||||
self.single_banner_var = tk.BooleanVar(value=False)
|
||||
self._check(self.single_tab, "Banner 探测", self.single_banner_var, 0, 3)
|
||||
actions = action_bar(self.single_tab, 1, 5)
|
||||
self.single_btn = button(actions, "测试连接", self.test_single, "Primary.TButton")
|
||||
self.single_export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||
|
||||
def build_scan_tab(self):
|
||||
for col in range(6):
|
||||
self.scan_tab.columnconfigure(col, weight=1)
|
||||
|
||||
self.scan_host = field(self.scan_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 26)
|
||||
self.scan_ports = field(self.scan_tab, "端口列表 / 范围", 0, 1, PORT_PRESETS["常用端口"], 44, colspan=2)
|
||||
self.scan_preset = combo(self.scan_tab, "预设", 0, 3, list(PORT_PRESETS), "常用端口", 16)
|
||||
self.scan_timeout = field(self.scan_tab, "超时 ms", 0, 4, "800", 10)
|
||||
self.scan_workers = field(self.scan_tab, "并发数", 0, 5, "128", 10)
|
||||
self.scan_show_closed_var = tk.BooleanVar(value=False)
|
||||
self.scan_banner_var = tk.BooleanVar(value=False)
|
||||
self._check(self.scan_tab, "显示关闭端口", self.scan_show_closed_var, 1, 0)
|
||||
self._check(self.scan_tab, "Banner 探测", self.scan_banner_var, 1, 1)
|
||||
self.scan_preset["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.apply_preset(self.scan_preset, self.scan_ports))
|
||||
|
||||
actions = action_bar(self.scan_tab, 2, 6)
|
||||
self.scan_start_btn = button(actions, "开始扫描", self.scan_ports_action, "Primary.TButton")
|
||||
self.scan_stop_btn = button(actions, "停止", self.stop_scan, "Danger.TButton")
|
||||
self.scan_copy_btn = button(actions, "复制开放端口", self.copy_open_ports, "Secondary.TButton")
|
||||
self.scan_export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||
|
||||
def build_batch_tab(self):
|
||||
for col in range(6):
|
||||
self.batch_tab.columnconfigure(col, weight=1)
|
||||
|
||||
self.batch_hosts = field(self.batch_tab, "目标列表 / CIDR / IP 段", 0, 0, "192.168.1.1-254", 38, colspan=2)
|
||||
self.batch_ports = field(self.batch_tab, "端口列表 / 范围", 0, 2, "22,80,443,3389", 32, colspan=2)
|
||||
self.batch_preset = combo(self.batch_tab, "预设", 0, 4, list(PORT_PRESETS), "远程管理", 16)
|
||||
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 5, "800", 10)
|
||||
self.batch_workers = field(self.batch_tab, "并发数", 1, 0, "128", 10)
|
||||
self.batch_show_closed_var = tk.BooleanVar(value=False)
|
||||
self.batch_banner_var = tk.BooleanVar(value=False)
|
||||
self._check(self.batch_tab, "显示关闭端口", self.batch_show_closed_var, 1, 1)
|
||||
self._check(self.batch_tab, "Banner 探测", self.batch_banner_var, 1, 2)
|
||||
self.batch_preset["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.apply_preset(self.batch_preset, self.batch_ports))
|
||||
|
||||
actions = action_bar(self.batch_tab, 2, 6)
|
||||
self.batch_start_btn = button(actions, "批量扫描", self.batch_scan_action, "Primary.TButton")
|
||||
self.batch_stop_btn = button(actions, "停止", self.stop_scan, "Danger.TButton")
|
||||
self.import_hosts_btn = button(actions, "导入目标", self.import_hosts, "Secondary.TButton")
|
||||
self.batch_copy_btn = button(actions, "复制开放端口", self.copy_open_ports, "Secondary.TButton")
|
||||
self.batch_export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||
|
||||
def _check(self, parent, text, variable, row, column):
|
||||
frame = ttk.Frame(parent, style="Panel.TFrame")
|
||||
frame.grid(row=row, column=column, sticky="ew", padx=18, pady=(22, 14))
|
||||
ttk.Checkbutton(frame, text=text, variable=variable).pack(anchor="w")
|
||||
|
||||
def write(self, text, tag=None):
|
||||
self.console.write(text, tag)
|
||||
|
||||
def clear(self):
|
||||
self.console.clear()
|
||||
for item in self.results_tree.get_children():
|
||||
self.results_tree.delete(item)
|
||||
self.update_status(
|
||||
{
|
||||
"state": "等待",
|
||||
"total": 0,
|
||||
"scanned": 0,
|
||||
"open": 0,
|
||||
"closed": 0,
|
||||
"timeout": 0,
|
||||
"progress": 0,
|
||||
"elapsed": 0,
|
||||
}
|
||||
)
|
||||
|
||||
def test_single(self):
|
||||
try:
|
||||
self.clear()
|
||||
self.single_btn.configure(state="disabled")
|
||||
self.scanner.test_connect(
|
||||
self.single_host["var"].get(),
|
||||
int(self.single_port["var"].get()),
|
||||
options={
|
||||
"timeout_ms": self.single_timeout["var"].get(),
|
||||
"workers": 1,
|
||||
"show_closed": True,
|
||||
"banner_probe": self.single_banner_var.get(),
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
self.single_btn.configure(state="normal")
|
||||
messagebox.showwarning("无法测试端口", str(exc))
|
||||
|
||||
def scan_ports_action(self):
|
||||
try:
|
||||
self.clear()
|
||||
self._set_scan_buttons("disabled")
|
||||
self.scanner.start_scan(self.scan_host["var"].get(), self.scan_ports["var"].get(), self.scan_options())
|
||||
except Exception as exc:
|
||||
self._set_scan_buttons("normal")
|
||||
messagebox.showwarning("无法扫描端口", str(exc))
|
||||
|
||||
def batch_scan_action(self):
|
||||
try:
|
||||
self.clear()
|
||||
self._set_scan_buttons("disabled")
|
||||
self.scanner.start_batch_scan(self.batch_hosts["var"].get(), self.batch_ports["var"].get(), self.batch_options())
|
||||
except Exception as exc:
|
||||
self._set_scan_buttons("normal")
|
||||
messagebox.showwarning("无法批量扫描", str(exc))
|
||||
|
||||
def stop_scan(self):
|
||||
try:
|
||||
self.scanner.stop_scan()
|
||||
except Exception as exc:
|
||||
messagebox.showinfo("提示", str(exc))
|
||||
|
||||
def scan_options(self):
|
||||
return {
|
||||
"timeout_ms": self.scan_timeout["var"].get(),
|
||||
"workers": self.scan_workers["var"].get(),
|
||||
"show_closed": self.scan_show_closed_var.get(),
|
||||
"banner_probe": self.scan_banner_var.get(),
|
||||
}
|
||||
|
||||
def batch_options(self):
|
||||
return {
|
||||
"timeout_ms": self.batch_timeout["var"].get(),
|
||||
"workers": self.batch_workers["var"].get(),
|
||||
"show_closed": self.batch_show_closed_var.get(),
|
||||
"banner_probe": self.batch_banner_var.get(),
|
||||
}
|
||||
|
||||
def apply_preset(self, preset_item, ports_item):
|
||||
ports_item["var"].set(PORT_PRESETS.get(preset_item["var"].get(), PORT_PRESETS["常用端口"]))
|
||||
|
||||
def import_hosts(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title="导入扫描目标",
|
||||
filetypes=[("文本文件", "*.txt *.csv"), ("所有文件", "*.*")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
port = int(self.port)
|
||||
except ValueError:
|
||||
messagebox.showwarning("输入错误", "端口必须是整数!")
|
||||
return
|
||||
if port < 1 or port > 65535:
|
||||
messagebox.showwarning("输入错误", "起始端口最小为1,结束端口最大为65535!")
|
||||
return
|
||||
logger.info(f"测试{self.IP} 的 {port} 端口连接情况")
|
||||
self.telnet_fun.test_connect(self.IP, port)
|
||||
with open(path, "r", encoding="utf-8-sig") as file:
|
||||
items = []
|
||||
for line in file:
|
||||
items.extend(part.strip() for part in line.replace(",", ",").split(",") if part.strip())
|
||||
self.batch_hosts["var"].set(",".join(items))
|
||||
self.write(f"已导入 {len(items)} 个目标/表达式\n", "success")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("导入失败", str(exc))
|
||||
|
||||
def btn_batchTelnet_start(self):
|
||||
self.IP = self.batchTelnet_IP['var'].get()
|
||||
self.port_begin = self.batchTelnet_port_begin['var'].get()
|
||||
self.port_end = self.batchTelnet_port_end['var'].get()
|
||||
if not self.IP:
|
||||
messagebox.showwarning("输入错误", "请输入目标IP或域名!")
|
||||
return
|
||||
if not self.port_begin or not self.port_end:
|
||||
messagebox.showwarning("输入错误", "请输入目标端口!")
|
||||
def export_results(self):
|
||||
path = filedialog.asksaveasfilename(
|
||||
title="导出端口扫描结果",
|
||||
defaultextension=".csv",
|
||||
filetypes=[("CSV 文件", "*.csv")],
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
port_begin = int(self.port_begin)
|
||||
port_end = int(self.port_end)
|
||||
except ValueError:
|
||||
messagebox.showwarning("输入错误", "端口必须是整数!")
|
||||
self.scanner.export_results(path)
|
||||
self.write(f"已导出结果: {path}\n", "success")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("导出失败", str(exc))
|
||||
|
||||
def copy_open_ports(self):
|
||||
text = self.scanner.open_ports_summary()
|
||||
if not text:
|
||||
messagebox.showinfo("提示", "没有可复制的开放端口")
|
||||
return
|
||||
if port_begin < 1 or port_end > 65535:
|
||||
messagebox.showwarning("输入错误", "起始端口最小为1,结束端口最大为65535!")
|
||||
return
|
||||
logger.info(f"开始测试{self.IP} 的 {port_begin} 到 {port_end} 端口连接情况")
|
||||
self.telnet_fun.start_range_scan(self.IP, port_begin, port_end)
|
||||
|
||||
def btn_batchTelnet_stop(self):
|
||||
logger.info(f"停止测试{self.IP} 的端口连接情况")
|
||||
self.telnet_fun.stop_scan()
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(text)
|
||||
self.write("已复制开放端口汇总到剪贴板\n", "success")
|
||||
|
||||
def btn_listTelnet_start(self):
|
||||
self.IP = self.listtelnet_IP['var'].get()
|
||||
self.ports = self.listtelnet_ports['var'].get()
|
||||
|
||||
if not self.ports:
|
||||
messagebox.showwarning("输入错误", "请输入端口号或范围!")
|
||||
return
|
||||
def update_status(self, stats):
|
||||
def apply():
|
||||
values = [
|
||||
(self.state, stats.get("state", "等待")),
|
||||
(self.total, str(stats.get("total", 0))),
|
||||
(self.scanned, str(stats.get("scanned", 0))),
|
||||
(self.open_count, str(stats.get("open", 0))),
|
||||
(self.closed_count, str(stats.get("closed", 0))),
|
||||
(self.timeout_count, str(stats.get("timeout", 0))),
|
||||
(self.progress, f"{stats.get('progress', 0):.1f}% / {stats.get('elapsed', 0):.1f}s"),
|
||||
]
|
||||
for item, value in values:
|
||||
item["entry"].configure(state="normal")
|
||||
item["var"].set(value)
|
||||
item["entry"].configure(state="disabled")
|
||||
|
||||
# 支持格式:例如 "22,80,443,8080"
|
||||
try:
|
||||
ports = [int(p.strip()) for p in self.ports.split(',') if p.strip()]
|
||||
except ValueError:
|
||||
messagebox.showwarning("输入错误", "端口必须为整数,用逗号分隔")
|
||||
return
|
||||
self.after(0, apply)
|
||||
|
||||
logger.info(f"开始测试{self.IP} 的 {ports} 端口连接情况")
|
||||
self.telnet_fun.start_list_scan(self.IP, ports)
|
||||
def add_result(self, row):
|
||||
def apply():
|
||||
port = row.get("port") or "-"
|
||||
latency = f"{row.get('latency_ms', 0):.1f} ms" if row.get("latency_ms") else "-"
|
||||
detail = row.get("banner") or row.get("error") or ""
|
||||
self.results_tree.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(
|
||||
row.get("host", ""),
|
||||
row.get("resolved_ip", ""),
|
||||
port,
|
||||
row.get("service", ""),
|
||||
row.get("status_text", ""),
|
||||
latency,
|
||||
detail,
|
||||
),
|
||||
tags=(row.get("status", ""),),
|
||||
)
|
||||
|
||||
def btn_listTelnet_stop(self):
|
||||
logger.info(f"停止测试{self.IP} 的端口连接情况")
|
||||
self.telnet_fun.stop_scan()
|
||||
self.after(0, apply)
|
||||
|
||||
def _set_scan_buttons(self, state):
|
||||
self.scan_start_btn.configure(state=state)
|
||||
self.batch_start_btn.configure(state=state)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def on_task_done(self):
|
||||
self.after(
|
||||
0,
|
||||
lambda: (
|
||||
self.single_btn.configure(state="normal"),
|
||||
self._set_scan_buttons("normal"),
|
||||
),
|
||||
)
|
||||
|
||||
+39
-45
@@ -1,56 +1,50 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
from tkinter import messagebox
|
||||
|
||||
from core.ui.basic_ui import BasicUI
|
||||
from core.Function.tracert_fun import TracertFun
|
||||
from core.Function.tracert_fun import TracertFun
|
||||
from core.ui.components import Console, Page, action_bar, button, field
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TracertTab(ttk.Frame, BasicUI):
|
||||
class TracertTab(Page):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.tracert_ui()
|
||||
self.tracert_fun = TracertFun(self.result_box)
|
||||
super().__init__(parent, "路由追踪", "查看从本机到目标地址的网络跳点和响应时间。")
|
||||
self.body.rowconfigure(1, weight=1)
|
||||
|
||||
def tracert_ui(self):
|
||||
"""tracert界面布局"""
|
||||
self.create_targetadd_section()
|
||||
target = self.section("追踪参数", 0, columns=4)
|
||||
self.target = field(target, "目标 IP / 域名", 1, 0, "8.8.8.8", 28)
|
||||
self.max_hops = field(target, "最大跳数", 1, 1, "20", 12)
|
||||
self.timeout_ms = field(target, "单跳超时(ms)", 1, 2, "800", 14)
|
||||
actions = action_bar(target, 2, 4)
|
||||
self.start_btn = button(actions, "↗ 开始追踪", self.start_trace, "Primary.TButton")
|
||||
self.stop_btn = button(actions, "■ 停止", self.stop_trace, "Danger.TButton")
|
||||
|
||||
self.create_output_section()
|
||||
# --------------------------------------UI界面布局函数--------------------------------------
|
||||
def create_targetadd_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="追踪目标地址")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
output = self.section("输出控制台", 1, columns=1)
|
||||
output.rowconfigure(1, weight=1)
|
||||
output.columnconfigure(0, weight=1)
|
||||
self.console = Console(output, height=22)
|
||||
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||
|
||||
self.entry_tracert_add = self.add_input(frame, "目标地址", row=0, col=0, entry_width=40, inivar="202.89.233.100")
|
||||
self.but_tracert_start = self.add_button(frame, "开始追踪", row=0, col=1, width=8, command=self.tracert_start_callback)
|
||||
self.but_tracert_stop = self.add_button(frame, "停止追踪", row=0, col=2, width=8, command=self.tracert_stop_callback)
|
||||
self.tracert_fun = TracertFun(self.write, self.on_task_done)
|
||||
|
||||
def create_output_section(self):
|
||||
# 区域标签
|
||||
frame = ttk.LabelFrame(self, text="结果输出")
|
||||
frame.pack(side='top', fill='x', padx=10, pady=5)
|
||||
def write(self, text, tag=None):
|
||||
self.console.write(text, tag)
|
||||
|
||||
self.result_box = scrolledtext.ScrolledText(frame, width=100, height=25)
|
||||
self.result_box.pack(pady=10)
|
||||
# --------------------------------------按钮回调函数--------------------------------------
|
||||
def tracert_start_callback(self):
|
||||
"""开始追踪按钮回调"""
|
||||
target = self.entry_tracert_add['var'].get()
|
||||
if not target:
|
||||
messagebox.showwarning("输入错误", "请输入目标地址!")
|
||||
return
|
||||
self.tracert_fun.start_tracert(target)
|
||||
def start_trace(self):
|
||||
try:
|
||||
self.console.clear()
|
||||
self.tracert_fun.start_tracert(
|
||||
self.target["var"].get(),
|
||||
int(self.max_hops["var"].get()),
|
||||
int(self.timeout_ms["var"].get()),
|
||||
)
|
||||
self.start_btn.configure(state="disabled")
|
||||
except Exception as exc:
|
||||
messagebox.showwarning("无法开始路由追踪", str(exc))
|
||||
|
||||
def tracert_stop_callback(self):
|
||||
"""停止追踪按钮回调"""
|
||||
self.tracert_fun.stop_tracert()
|
||||
def stop_trace(self):
|
||||
try:
|
||||
self.tracert_fun.stop_tracert()
|
||||
except Exception as exc:
|
||||
messagebox.showinfo("提示", str(exc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def on_task_done(self):
|
||||
self.after(0, lambda: self.start_btn.configure(state="normal"))
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
|
||||
COLORS = {
|
||||
"bg": "#eef3f8",
|
||||
"panel": "#ffffff",
|
||||
"panel_alt": "#f7f9fc",
|
||||
"sidebar": "#102033",
|
||||
"sidebar_hover": "#1a3552",
|
||||
"sidebar_active": "#246bfe",
|
||||
"text": "#172033",
|
||||
"muted": "#607086",
|
||||
"border": "#dbe4ef",
|
||||
"primary": "#246bfe",
|
||||
"primary_dark": "#1451c8",
|
||||
"danger": "#df3b3b",
|
||||
"success": "#1a9b6c",
|
||||
"warning": "#b7791f",
|
||||
"console_bg": "#0f1724",
|
||||
"console_fg": "#dbeafe",
|
||||
"console_muted": "#9fb4d0",
|
||||
}
|
||||
|
||||
|
||||
FONT_BODY = ("Microsoft YaHei UI", 10)
|
||||
FONT_SMALL = ("Microsoft YaHei UI", 9)
|
||||
FONT_TITLE = ("Microsoft YaHei UI", 17, "bold")
|
||||
FONT_SECTION = ("Microsoft YaHei UI", 11, "bold")
|
||||
FONT_MONO = ("Consolas", 10)
|
||||
|
||||
|
||||
def apply_theme(root: tk.Tk) -> None:
|
||||
root.configure(bg=COLORS["bg"])
|
||||
|
||||
style = ttk.Style(root)
|
||||
try:
|
||||
style.theme_use("clam")
|
||||
except tk.TclError:
|
||||
pass
|
||||
|
||||
style.configure(".", font=FONT_BODY)
|
||||
style.configure("TFrame", background=COLORS["bg"])
|
||||
style.configure("Panel.TFrame", background=COLORS["panel"])
|
||||
style.configure("Alt.TFrame", background=COLORS["panel_alt"])
|
||||
style.configure("TLabel", background=COLORS["panel"], foreground=COLORS["text"])
|
||||
style.configure("Muted.TLabel", background=COLORS["panel"], foreground=COLORS["muted"], font=FONT_SMALL)
|
||||
style.configure("Title.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_TITLE)
|
||||
style.configure("Section.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_SECTION)
|
||||
|
||||
style.configure(
|
||||
"TEntry",
|
||||
fieldbackground="#ffffff",
|
||||
foreground=COLORS["text"],
|
||||
bordercolor=COLORS["border"],
|
||||
lightcolor=COLORS["border"],
|
||||
darkcolor=COLORS["border"],
|
||||
padding=7,
|
||||
)
|
||||
style.configure(
|
||||
"TCombobox",
|
||||
fieldbackground="#ffffff",
|
||||
foreground=COLORS["text"],
|
||||
bordercolor=COLORS["border"],
|
||||
arrowcolor=COLORS["muted"],
|
||||
padding=6,
|
||||
)
|
||||
|
||||
style.configure(
|
||||
"Primary.TButton",
|
||||
background=COLORS["primary"],
|
||||
foreground="#ffffff",
|
||||
borderwidth=0,
|
||||
focusthickness=0,
|
||||
padding=(14, 8),
|
||||
)
|
||||
style.map("Primary.TButton", background=[("active", COLORS["primary_dark"]), ("disabled", "#9bb7f5")])
|
||||
|
||||
style.configure(
|
||||
"Secondary.TButton",
|
||||
background="#e7eef8",
|
||||
foreground=COLORS["text"],
|
||||
borderwidth=0,
|
||||
padding=(14, 8),
|
||||
)
|
||||
style.map("Secondary.TButton", background=[("active", "#d6e2f3")])
|
||||
|
||||
style.configure(
|
||||
"Danger.TButton",
|
||||
background=COLORS["danger"],
|
||||
foreground="#ffffff",
|
||||
borderwidth=0,
|
||||
padding=(14, 8),
|
||||
)
|
||||
style.map("Danger.TButton", background=[("active", "#bd2929")])
|
||||
|
||||
|
||||
def build_app_icon(size: int = 64) -> tk.PhotoImage:
|
||||
image = tk.PhotoImage(width=size, height=size)
|
||||
image.put(COLORS["sidebar"], to=(0, 0, size, size))
|
||||
image.put(COLORS["primary"], to=(8, 8, size - 8, size - 8))
|
||||
image.put("#5eead4", to=(16, 20, 27, 31))
|
||||
image.put("#ffffff", to=(37, 20, 48, 31))
|
||||
image.put("#ffffff", to=(16, 38, 27, 49))
|
||||
image.put("#5eead4", to=(37, 38, 48, 49))
|
||||
image.put("#ffffff", to=(24, 25, 41, 29))
|
||||
image.put("#ffffff", to=(24, 42, 41, 46))
|
||||
image.put("#ffffff", to=(21, 28, 25, 42))
|
||||
image.put("#ffffff", to=(39, 28, 43, 42))
|
||||
return image
|
||||
+115
-22
@@ -1,31 +1,124 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
from tkinter import ttk
|
||||
|
||||
from core.ui.basic_ui import BasicUI
|
||||
from core.ui.tab_network import NetworkTab
|
||||
from core.ui.tab_ping import PingTab
|
||||
from core.ui.tab_telnet import TelnetTab
|
||||
from core.ui.tab_network import NetworkTab
|
||||
from core.ui.tab_tracert import TracertTab
|
||||
from core.ui.theme import COLORS, apply_theme, build_app_icon
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MainUI(BasicUI):
|
||||
def __init__(self, root):
|
||||
class MainUI:
|
||||
def __init__(self, root: tk.Tk, base_dir: str = ""):
|
||||
self.root = root
|
||||
self.root.title("Network tools") # 窗口标题
|
||||
self.root.geometry("800x500") # 窗口大小
|
||||
root.resizable(False, False) # 禁止水平和垂直调整大小
|
||||
# 创建 Notebook 作为多标签页容器
|
||||
self.tab_control = ttk.Notebook(root)
|
||||
self.tab_control.pack(expand=1, fill="both")
|
||||
# 实例化各个功能页
|
||||
self.tabs = {
|
||||
"网卡设置": NetworkTab(self.tab_control),
|
||||
"Ping测试": PingTab(self.tab_control),
|
||||
"端口扫描": TelnetTab(self.tab_control),
|
||||
"路由追踪": TracertTab(self.tab_control),
|
||||
self.base_dir = base_dir
|
||||
self.root.title("NetPilot 网络调试工具")
|
||||
self.root.geometry("1160x720")
|
||||
apply_theme(root)
|
||||
|
||||
self.icon_image = build_app_icon()
|
||||
self.root.iconphoto(True, self.icon_image)
|
||||
|
||||
self.root.columnconfigure(1, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
|
||||
self.sidebar = tk.Frame(root, width=232, bg=COLORS["sidebar"])
|
||||
self.sidebar.grid(row=0, column=0, sticky="ns")
|
||||
self.sidebar.grid_propagate(False)
|
||||
|
||||
self.content = ttk.Frame(root, style="Panel.TFrame")
|
||||
self.content.grid(row=0, column=1, sticky="nsew")
|
||||
self.content.rowconfigure(0, weight=1)
|
||||
self.content.columnconfigure(0, weight=1)
|
||||
|
||||
self.pages = {}
|
||||
self.nav_buttons = {}
|
||||
self._build_sidebar()
|
||||
self._build_pages()
|
||||
self.show_page("network")
|
||||
|
||||
def _build_sidebar(self) -> None:
|
||||
brand = tk.Frame(self.sidebar, bg=COLORS["sidebar"])
|
||||
brand.pack(fill="x", padx=20, pady=(24, 26))
|
||||
|
||||
logo = tk.Label(
|
||||
brand,
|
||||
text="NP",
|
||||
width=4,
|
||||
height=2,
|
||||
bg=COLORS["primary"],
|
||||
fg="#ffffff",
|
||||
font=("Microsoft YaHei UI", 14, "bold"),
|
||||
)
|
||||
logo.pack(side="left")
|
||||
|
||||
title = tk.Frame(brand, bg=COLORS["sidebar"])
|
||||
title.pack(side="left", padx=12)
|
||||
tk.Label(title, text="NetPilot", bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 16, "bold")).pack(anchor="w")
|
||||
tk.Label(title, text="网络调试控制台", bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 9)).pack(anchor="w")
|
||||
|
||||
items = [
|
||||
("network", "◎", "网卡配置", "IP / DNS / DHCP"),
|
||||
("ping", "⌁", "Ping 探测", "单点与批量探活"),
|
||||
("ports", "⌕", "端口扫描", "TCP 连通性检测"),
|
||||
("trace", "↗", "路由追踪", "跳点路径分析"),
|
||||
]
|
||||
for key, icon, title, subtitle in items:
|
||||
self.nav_buttons[key] = self._nav_button(key, icon, title, subtitle)
|
||||
|
||||
footer = tk.Label(
|
||||
self.sidebar,
|
||||
text="v2 重构版",
|
||||
bg=COLORS["sidebar"],
|
||||
fg="#7185a0",
|
||||
font=("Microsoft YaHei UI", 9),
|
||||
)
|
||||
footer.pack(side="bottom", anchor="w", padx=22, pady=20)
|
||||
|
||||
def _nav_button(self, key: str, icon: str, title: str, subtitle: str) -> tk.Frame:
|
||||
frame = tk.Frame(self.sidebar, bg=COLORS["sidebar"], cursor="hand2")
|
||||
frame.pack(fill="x", padx=14, pady=4)
|
||||
|
||||
icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#c8d7ec", font=("Microsoft YaHei UI", 18))
|
||||
icon_label.pack(side="left", padx=(8, 6), pady=10)
|
||||
|
||||
text_frame = tk.Frame(frame, bg=COLORS["sidebar"])
|
||||
text_frame.pack(side="left", fill="x", expand=True)
|
||||
title_label = tk.Label(text_frame, text=title, bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 10, "bold"))
|
||||
title_label.pack(anchor="w")
|
||||
subtitle_label = tk.Label(text_frame, text=subtitle, bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 8))
|
||||
subtitle_label.pack(anchor="w", pady=(2, 0))
|
||||
|
||||
widgets = (frame, icon_label, text_frame, title_label, subtitle_label)
|
||||
for widget in widgets:
|
||||
widget.bind("<Button-1>", lambda _event, page=key: self.show_page(page))
|
||||
widget.bind("<Enter>", lambda _event, widgets=widgets, page=key: self._paint_nav(widgets, page, hover=True))
|
||||
widget.bind("<Leave>", lambda _event, widgets=widgets, page=key: self._paint_nav(widgets, page, hover=False))
|
||||
frame._nav_widgets = widgets
|
||||
return frame
|
||||
|
||||
def _build_pages(self) -> None:
|
||||
self.pages = {
|
||||
"network": NetworkTab(self.content),
|
||||
"ping": PingTab(self.content),
|
||||
"ports": TelnetTab(self.content),
|
||||
"trace": TracertTab(self.content),
|
||||
}
|
||||
# 添加到 Notebook
|
||||
for name, tab in self.tabs.items():
|
||||
self.tab_control.add(tab, text=name)
|
||||
for page in self.pages.values():
|
||||
page.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
def show_page(self, key: str) -> None:
|
||||
self.active_page = key
|
||||
self.pages[key].tkraise()
|
||||
for nav_key, frame in self.nav_buttons.items():
|
||||
self._paint_nav(frame._nav_widgets, nav_key, hover=False)
|
||||
|
||||
def _paint_nav(self, widgets, key: str, hover: bool) -> None:
|
||||
active = getattr(self, "active_page", None) == key
|
||||
bg = COLORS["sidebar_active"] if active else COLORS["sidebar_hover"] if hover else COLORS["sidebar"]
|
||||
fg = "#ffffff" if active or hover else "#c8d7ec"
|
||||
muted = "#dce9ff" if active else "#9fb4d0"
|
||||
for index, widget in enumerate(widgets):
|
||||
widget.configure(bg=bg)
|
||||
if isinstance(widget, tk.Label):
|
||||
widget.configure(fg=fg if index in (1, 3) else muted)
|
||||
|
||||
Reference in New Issue
Block a user