电脑IP地址快速切换

实用软件1周前更新 小编
4 0

溜达时到软件区看到一个换ip的软件,正好最近做的项目有配置摄像头,没有专业一些工具,要求比较多,利旧的设备和新加的摄像头配置需要反复改电脑IP,手动操作更改ip非常繁琐。
之前其他大佬们分享的软件有收集用过,一个是每次也需要手动去输入IP地址,以旧麻烦,另外一款的软件界面非常好用,可以先添加好需要网段的ip配置,然后通过点击来切换,非常好的点子,但是介于我想更加偷懒和强迫症的心情,请手下大将帮忙搞了一个,感觉很好用,分享给大家。主要是美化了一下界面,然后缩小软件桌面占比,然后可以置顶,并且靠近边框可以自动缩小成小图标,可以通过双击预先添加的ip方案来快速切换电脑的IP地址,极大的加快了干活的时间,希望对大家有用。
界面如下:
电脑IP地址快速切换

1.可以检测电脑的网卡,然后选择需要修改的网卡,例如以太网卡和无线网卡;
2.可以自己添加需要添加的需要的IP地址进去,通过双击已添加的方案可以快速更改本机的ip地址;
电脑IP地址快速切换

3.窗口默认桌面置顶,方便快速切换;
4.软件窗口靠近桌面窗口边缘可自动缩小成悬浮小图标。
电脑IP地址快速切换

5.右击软件界面即可退出软件。

软件是用python写的,大家可直接用我打包的软件,也可以自行编译打包。已经再win10-11上测试能够正常使用。
通过网盘分享的文件:IP地址快速修改1.0
链接:
源码:

  1. # -*- coding: utf-8 -*-
  2. """ IP 快速切换工具 - 暗色半透明悬浮窗版
  3.     依赖: pip install customtkinter
  4.     无标题栏 / 默认置顶 / 靠边自动收成小图标 / 每行2个IP / 黑色半透明
  5. """
  6. import os, sys, json, threading, subprocess, ctypes, math
  7. import ctypes.wintypes as wt
  8. import tkinter as tk
  9. import customtkinter as ctk
  10. # ---------------- 可调常量 ----------------
  11. W = 300                 # 窗口固定宽度
  12. CARD_H = 38             # 卡片高度
  13. ROW_STEP = 44           # 每行卡片占用高度(含间距)
  14. HEADER_H = 98           # 顶部固定区高度(拖动条+适配器行+标题行+底边距)
  15. WINDOW_ALPHA = 0.90     # 窗口半透明度(0~1, 越小越透)
  16. THR = 10                # 贴边判定阈值(px)
  17. AWAY_MS = 1200          # 鼠标离开贴边窗口多久后收起
  18. FONT_FAMILY = "Microsoft YaHei UI"
  19. CREATE_NO_WINDOW = 0x08000000
  20. # ---------------- 暗色玻璃配色 ----------------
  21. WIN_BG     = "#14171c"   # 窗口半透明黑底
  22. DRAG_GRIP  = "#4b525c"   # 拖动条抓手
  23. TXT        = "#e8ebf0"   # 主文字
  24. SUB        = "#9aa1ab"   # 次文字
  25. BLUE       = "#3b82f6";  BLUE_H   = "#2f6fe0"
  26. GREEN      = "#22c55e";  GREEN_H  = "#16a34a"
  27. DBTN       = "#262b33";  DBTN_H   = "#323842"   # 深灰按钮
  28. DBTN_DEL_H = "#3a2226"                          # 删除按钮hover
  29. RED        = "#ff6b6b"
  30. CARD       = "#20242b";  CARD_B     = "#333a44"
  31. CARD_SEL   = "#262c36";  CARD_SEL_B = "#5b8def"
  32. CARD_ACT   = "#1d2942";  CARD_ACT_B = "#3b82f6"
  33. DOT        = "#5a626d"
  34. TOAST_BG   = "#2563eb"
  35. def F(size, weight="normal"):
  36.     return ctk.CTkFont(family=FONT_FAMILY, size=size, weight=weight)
  37. # ---------------- 系统工具 ----------------
  38. def is_admin():
  39.     try: return bool(ctypes.windll.shell32.IsUserAnAdmin())
  40.     except Exception: return False
  41. def run_as_admin():
  42.     if getattr(sys, "frozen", False):
  43.         exe, params = sys.executable, subprocess.list2cmdline(sys.argv[1:])
  44.     else:
  45.         exe = sys.executable.replace("python.exe", "pythonw.exe")
  46.         if not os.path.exists(exe): exe = sys.executable
  47.         params = subprocess.list2cmdline(sys.argv)
  48.     ctypes.windll.shell32.ShellExecuteW(None, "runas", exe, params, None, 1)
  49.     sys.exit(0)
  50. def cursor_pos():
  51.     p = wt.POINT(); ctypes.windll.user32.GetCursorPos(ctypes.byref(p)); return p.x, p.y
  52. def workarea():
  53.     r = wt.RECT(); ctypes.windll.user32.SystemParametersInfoW(0x0030, 0, ctypes.byref(r), 0)
  54.     return r.left, r.top, r.right, r.bottom
  55. def clamp(v, lo, hi): return max(lo, min(hi, v))
  56. # ---------------- 网络操作 ----------------
  57. PS_SCRIPT = r"""
  58. [Console]::OutputEncoding=[Text.UTF8Encoding]::new()
  59. function P2M($p){
  60.   if($p -le 0){return '0.0.0.0'}; if($p -ge 32){return '255.255.255.255'}
  61.   $m=[uint32](((1 -shl 32) - 1) -shl (32-$p))
  62.   return ("{0}.{1}.{2}.{3}" -f (($m -shr 24) -band 255),(($m -shr 16) -band 255),(($m -shr 8) -band 255),($m -band 255))
  63. }
  64. $out=@()
  65. Get-NetAdapter | Where-Object {$_.HardwareInterface -eq $true -and $_.Status -ne 'Disabled'} | ForEach-Object {
  66.   $n=$_.Name
  67.   $ipObj=Get-NetIPAddress -InterfaceAlias $n -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1
  68.   $gw=(Get-NetRoute -InterfaceAlias $n -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Select-Object -First 1).NextHop
  69.   $dns=(Get-DnsClientServerAddress -InterfaceAlias $n -AddressFamily IPv4 -ErrorAction SilentlyContinue).ServerAddresses
  70.   if($null -eq $dns){$dns=@()}else{$dns=@($dns)}
  71.   $dhcp = if($ipObj){$ipObj.PrefixOrigin -eq 'Dhcp'}else{$false}
  72.   $ip = if($ipObj){$ipObj.IPAddress}else{''}
  73.   $mask = if($ipObj){P2M $ipObj.PrefixLength}else{''}
  74.   $out += [ordered]@{name=$n; ip=$ip; mask=$mask; gateway=$gw; dns=$dns; dhcp=$dhcp}
  75. }
  76. [pscustomobject]@{adapters=$out} | ConvertTo-Json -Depth 5
  77. """
  78. class NetManager:
  79.     def _run(self, args):
  80.         r = subprocess.run(args, creationflags=CREATE_NO_WINDOW,
  81.                            capture_output=True, timeout=20)
  82.         if r.returncode != 0:
  83.             err = (r.stderr or b"").decode("gbk", "ignore").strip()
  84.             raise RuntimeError(err or f"命令失败 code={r.returncode}")
  85.         return r
  86.     def get_all(self):
  87.         r = self._run(["powershell", "-NoProfile", "-Command", PS_SCRIPT])
  88.         data = json.loads(r.stdout.decode("utf-8", "ignore") or "{}")
  89.         ad = data.get("adapters") or []
  90.         if isinstance(ad, dict): ad =
    广告也精彩
  91.         net = {}
  92.         for a in ad:
  93.             net[a.get("name")] = {
  94.                 "ip": a.get("ip") or "", "mask": a.get("mask") or "",
  95.                 "gateway": a.get("gateway") or "", "dns": a.get("dns") or [],
  96.                 "dhcp": bool(a.get("dhcp")),
  97.             }
  98.         return net
  99.     def apply(self, adapter, p):
  100.         n = f"name={adapter}"
  101.         if p.get("dhcp"):
  102.             self._run(["netsh", "interface", "ip", "set", "address", n, "dhcp"])
  103.             self._run(["netsh", "interface", "ip", "set", "dns", n, "dhcp"])
  104.             return
  105.         ip = (p.get("ip") or "").strip()
  106.         if not ip: raise RuntimeError("静态方案缺少 IP")
  107.         mask = (p.get("mask") or "255.255.255.0").strip()
  108.         gw = (p.get("gateway") or "").strip()
  109.         cmd = ["netsh", "interface", "ip", "set", "address", n, "static", ip, mask]
  110.         if gw: cmd += [gw, "1"]
  111.         self._run(cmd)
  112.         dns = [d.strip() for d in (p.get("dns") or []) if d.strip()]
  113.         if dns:
  114.             self._run(["netsh", "interface", "ip", "set", "dns", n, "static", dns[0]])
  115.             for d in dns[1:]:
  116.                 self._run(["netsh", "interface", "ip", "add", "dns", n, f"addr={d}"])
  117.         else:
  118.             self._run(["netsh", "interface", "ip", "set", "dns", n, "dhcp"])
  119. # ---------------- 配置存储 ----------------
  120. def config_path():
  121.     base = os.path.dirname(sys.executable if getattr(sys, "frozen", False)
  122.                            else os.path.abspath(__file__))
  123.     p = os.path.join(base, "ip_switcher_config.json")
  124.     try:
  125.         with open(p, "a"): pass
  126.         return p
  127.     except Exception:
  128.         d = os.path.join(os.environ.get("APPDATA", base), "IPSwitcher")
  129.         os.makedirs(d, exist_ok=True)
  130.         return os.path.join(d, "ip_switcher_config.json")
  131. class Config:
  132.     def __init__(self):
  133.         self.path = config_path()
  134.         self.data = {"adapter": "", "profiles": []}
  135.         try:
  136.             with open(self.path, "r", encoding="utf-8") as f:
  137.                 self.data.update(json.load(f))
  138.         except Exception: pass
  139.         self.data.setdefault("profiles", [])
  140.     def save(self):
  141.         try:
  142.             with open(self.path, "w", encoding="utf-8") as f:
  143.                 json.dump(self.data, f, ensure_ascii=False, indent=2)
  144.         except Exception: pass
  145. # ---------------- 主界面 ----------------
  146. class App:
  147.     def __init__(self, root):
  148.         self.root = root
  149.         self.netmgr = NetManager()
  150.         self.config = Config()
  151.         self.net = {}
  152.         self.selected = None
  153.         self.card_widgets = []
  154.         self.state = "shown"
  155.         self.dock_enabled = False
  156.         self._away = 0
  157.         self._dock_side = None
  158.         self._dock_geom = None
  159.         self._drag = None
  160.         l, t, r, b = workarea()
  161.         self.root.title("IPSwitcher")
  162.         self.root.overrideredirect(True)
  163.         self.root.configure(fg_color=WIN_BG)
  164.         self.root.geometry(f"{W}x140+{r - W - 60}+{t + 40}")
  165.         self.root.attributes("-topmost", True)        # 默认置顶
  166.         self.root.attributes("-alpha", WINDOW_ALPHA)  # 黑色半透明
  167.         self._build_dragbar()
  168.         self._build_body()
  169.         self._build_dock_icon()
  170.         self._build_menu()
  171.         self._bind_rightclick(self.root)              # 全窗右键菜单
  172.         self.root.after(150, self._tick)
  173.         self.root.after(3000, self._enable_dock)
  174.         self._run_async(self.netmgr.get_all, self._on_first_load)
  175.     # ---- 异步执行 ----
  176.     def _run_async(self, fn, on_done):
  177.         def w():
  178.             try: r, e = fn(), None
  179.             except Exception as ex: r, e = None, ex
  180.             self.root.after(0, lambda: on_done(r, e))
  181.         threading.Thread(target=w, daemon=True).start()
  182.     # ---- 透明拖动条(无按钮/无文字, 仅抓手) ----
  183.     def _build_dragbar(self):
  184.         db = ctk.CTkFrame(self.root, height=16, corner_radius=0, fg_color="transparent")
  185.         db.pack(fill="x"); db.pack_propagate(False)
  186.         grip = ctk.CTkFrame(db, width=34, height=4, corner_radius=2, fg_color=DRAG_GRIP)
  187.         grip.pack(expand=True)
  188.         for w in (db, grip):
  189.             w.bind("<Button-1>", self._drag_start)
  190.             w.bind("<B1-Motion>", self._drag_move)
  191.             w.bind("<Enter>", lambda e: self.root.configure(cursor="fleur"))
  192.             w.bind("<Leave>", lambda e: self.root.configure(cursor=""))
  193.     def _drag_start(self, e):
  194.         self._drag = (e.x_root, e.y_root, self.root.winfo_x(), self.root.winfo_y())
  195.     def _drag_move(self, e):
  196.         if not self._drag: return
  197.         x0, y0, wx, wy = self._drag
  198.         self.root.geometry(f"+{wx + e.x_root - x0}+{wy + e.y_root - y0}")
  199.     # ---- 主体 ----
  200.     def _build_body(self):
  201.         body = ctk.CTkFrame(self.root, fg_color="transparent", corner_radius=0)
  202.         body.pack(fill="x")
  203.         f1 = ctk.CTkFrame(body, fg_color="transparent"); f1.pack(fill="x", padx=10, pady=(8, 4))
  204.         self.adapter_menu = ctk.CTkOptionMenu(f1, values=["(无适配器)"],
  205.             command=self._on_adapter_change, width=120, height=30, font=F(12),
  206.             dropdown_font=F(12))
  207.         self.adapter_menu.pack(side="left", fill="x", expand=True, padx=(0, 6))
  208.         ctk.CTkButton(f1, text="↻", width=34, height=30, fg_color=DBTN,
  209.             hover_color=DBTN_H, text_color=TXT, font=F(15),
  210.             command=self._on_refresh_click).pack(side="left", padx=(0, 6))
  211.         ctk.CTkButton(f1, text="应用", width=56, height=30, fg_color=BLUE,
  212.             hover_color=BLUE_H, font=F(12, "bold"),
  213.             command=self._on_apply_selected).pack(side="left")
  214.         f2 = ctk.CTkFrame(body, fg_color="transparent"); f2.pack(fill="x", padx=10, pady=(4, 4))
  215.         ctk.CTkLabel(f2, text="IP 方案   双击切换", text_color=SUB,
  216.             font=F(11)).pack(side="left")
  217.         ctk.CTkButton(f2, text="删", width=30, height=24, fg_color=DBTN,
  218.             hover_color=DBTN_DEL_H, text_color=RED, font=F(11),
  219.             command=self._on_delete).pack(side="right", padx=(2, 0))
  220.         ctk.CTkButton(f2, text="改", width=30, height=24, fg_color=DBTN,
  221.             hover_color=DBTN_H, text_color=TXT, font=F(11),
  222.             command=self._on_edit).pack(side="right", padx=(2, 0))
  223.         ctk.CTkButton(f2, text="+", width=30, height=24, fg_color=GREEN,
  224.             hover_color=GREEN_H, text_color="white", font=F(13, "bold"),
  225.             command=self._on_add).pack(side="right")
  226.         self.card_container = ctk.CTkFrame(body, fg_color="transparent")
  227.         self.card_container.pack(fill="x", padx=8, pady=(0, 8))
  228.         self.card_container.columnconfigure(0, weight=1)   # 固定2列, 宽度自适应
  229.         self.card_container.columnconfigure(1, weight=1)
  230.         self.toast = ctk.CTkLabel(self.root, text="", fg_color=TOAST_BG,
  231.             text_color="white", corner_radius=6, font=F(11))
  232.     # ---- 卡片(只显示IP, 宽度自适应) ----
  233.     def _build_cards(self):
  234.         for w in self.card_container.winfo_children(): w.destroy()
  235.         self.card_widgets = []
  236.         for i, p in enumerate(self.config.data["profiles"]):
  237.             self.card_widgets.append(self._make_card(i, p))
  238.         self._fit()
  239.         self._update_highlight()
  240.     def _make_card(self, i, p):
  241.         card = ctk.CTkFrame(self.card_container, height=CARD_H,
  242.             corner_radius=8, border_width=1, fg_color=CARD, border_color=CARD_B)
  243.         card.pack_propagate(False)
  244.         inner = ctk.CTkFrame(card, fg_color="transparent")
  245.         inner.pack(expand=True, fill="x", padx=8)
  246.         dot = ctk.CTkLabel(inner, text="●", text_color=DOT, font=F(10), width=14)
  247.         dot.pack(side="left")
  248.         lab = ctk.CTkLabel(inner, text=p.get("label", ""), text_color=TXT,
  249.             font=F(12, "bold"), anchor="w")
  250.         lab.pack(side="left", padx=(2, 0))
  251.         for w in (card, inner, dot, lab):
  252.             w.bind("<Button-1>", lambda e, i=i: self._select(i))
  253.             w.bind("<Double-Button-1>", lambda e, i=i: self._apply_index(i))
  254.         card._index, card._dot, card._lab = i, dot, lab
  255.         return card
  256.     def _rearrange(self):
  257.         for i, card in enumerate(self.card_widgets):     # 固定每行2个
  258.             r, c = divmod(i, 2)
  259.             card.grid(row=r, column=c, sticky="ew", padx=3, pady=3)
  260.         self.card_container.update_idletasks()
  261.     def _cur(self):
  262.         return self.net.get(self._cur_adapter())
  263.     def _cur_adapter(self):
  264.         a = self.config.data.get("adapter")
  265.         if a in self.net: return a
  266.         return next(iter(self.net), "")
  267.     def _match(self, p):
  268.         cur = self._cur()
  269.         if not cur: return False
  270.         if p.get("dhcp"): return bool(cur.get("dhcp"))
  271.         return (not cur.get("dhcp")) and cur.get("ip") == p.get("ip")
  272.     def _update_highlight(self):
  273.         for card in self.card_widgets:
  274.             p = self.config.data["profiles"][card._index]
  275.             act = self._match(p); sel = (card._index == self.selected)
  276.             if act:
  277.                 card.configure(fg_color=CARD_ACT, border_color=CARD_ACT_B)
  278.                 card._dot.configure(text_color=BLUE)
  279.             elif sel:
  280.                 card.configure(fg_color=CARD_SEL, border_color=CARD_SEL_B)
  281.                 card._dot.configure(text_color=DOT)
  282.             else:
  283.                 card.configure(fg_color=CARD, border_color=CARD_B)
  284.                 card._dot.configure(text_color=DOT)
  285.     def _select(self, i):
  286.         self.selected = i; self._update_highlight()
  287.     def _apply_index(self, i):
  288.         self.selected = i
  289.         self._do_apply(self.config.data["profiles"][i])
  290.     def _do_apply(self, p):
  291.         adapter = self._cur_adapter()
  292.         if not adapter:
  293.             self._toast("没有可用适配器"); return
  294.         self._toast("应用中…")
  295.         self._run_async(lambda: self.netmgr.apply(adapter, p),
  296.             lambda r, e: (self._toast("已应用 ✔" if not e else "失败: " + str(e)),
  297.                           self._run_async(self.netmgr.get_all, self._on_refresh)))
  298.     # ---- 顶部按钮回调 ----
  299.     def _on_adapter_change(self, name):
  300.         if name in ("(无适配器)",): return
  301.         self.config.data["adapter"] = name; self.config.save()
  302.         self._update_highlight()
  303.     def _on_refresh_click(self):
  304.         self._toast("刷新中…")
  305.         self._run_async(self.netmgr.get_all, self._on_refresh)
  306.     def _on_apply_selected(self):
  307.         if self.selected is None:
  308.             self._toast("请先单击/双击选中一个方案"); return
  309.         self._do_apply(self.config.data["profiles"][self.selected])
  310.     def _on_add(self): self._edit_dialog(None)
  311.     def _on_edit(self):
  312.         if self.selected is None: self._toast("先选中一个方案"); return
  313.         self._edit_dialog(self.selected)
  314.     def _on_delete(self):
  315.         if self.selected is None: self._toast("先选中一个方案"); return
  316.         self.config.data["profiles"].pop(self.selected)
  317.         self.selected = None; self.config.save(); self._build_cards()
  318.     # ---- 编辑弹窗 ----
  319.     def _edit_dialog(self, index):
  320.         editing = index is not None
  321.         p = dict(self.config.data["profiles"][index]) if editing else \
  322.             {"dhcp": False, "ip": "", "mask": "255.255.255.0", "gateway": "", "dns": []}
  323.         win = ctk.CTkToplevel(self.root)
  324.         win.title("编辑方案" if editing else "新增方案")
  325.         win.attributes("-topmost", True); win.transient(self.root)
  326.         win.geometry(f"320x280+{self.root.winfo_x()}+{self.root.winfo_y() + 30}")
  327.         win.grab_set()
  328.         var = tk.StringVar(value="on" if p.get("dhcp") else "off")
  329.         rows = []
  330.         def row(label, key, placeholder=""):
  331.             f = ctk.CTkFrame(win, fg_color="transparent"); f.pack(fill="x", padx=14, pady=3)
  332.             ctk.CTkLabel(f, text=label, width=46, anchor="w", font=F(12)).pack(side="left")
  333.             e = ctk.CTkEntry(f, height=30, font=F(12), placeholder_text=placeholder)
  334.             e.pack(side="left", fill="x", expand=True)
  335.             e.insert(0, str(p.get(key, "") or ""))
  336.             rows.append((key, e)); return e
  337.         cf = ctk.CTkFrame(win, fg_color="transparent"); cf.pack(fill="x", padx=14, pady=(12, 6))
  338.         cb = ctk.CTkCheckBox(cf, text="自动获取 IP (DHCP)", variable=var,
  339.             onvalue="on", offvalue="off", font=F(12), command=lambda: toggle())
  340.         cb.pack(side="left")
  341.         e_ip = row("IP", "ip", "192.168.1.100")
  342.         e_mask = row("掩码", "mask", "255.255.255.0")
  343.         e_gw = row("网关", "gateway", "192.168.1.1")
  344.         e_dns = row("DNS", "dns", "8.8.8.8, 114.114.114.114")
  345.         def toggle():
  346.             st = "disabled" if var.get() == "on" else "normal"
  347.             for _, e in rows: e.configure(state=st)
  348.         toggle()
  349.         bf = ctk.CTkFrame(win, fg_color="transparent"); bf.pack(fill="x", padx=14, pady=12)
  350.         def save():
  351.             dhcp = var.get() == "on"
  352.             ip = e_ip.get().strip(); mask = e_mask.get().strip() or "255.255.255.0"
  353.             gw = e_gw.get().strip()
  354.             dns = [d.strip() for d in e_dns.get().replace(";", ",").replace(";", ",").split(",") if d.strip()]
  355.             if not dhcp and not ip:
  356.                 self._toast("静态方案必须填写 IP"); return
  357.             label = "自动获取(DHCP)" if dhcp else (ip or "未设置IP")
  358.             new = {"label": label, "dhcp": dhcp, "ip": ip, "mask": mask, "gateway": gw, "dns": dns}
  359.             if editing: self.config.data["profiles"][index] = new; si = index
  360.             else: self.config.data["profiles"].append(new); si = len(self.config.data["profiles"]) - 1
  361.             self.config.save(); self.selected = si
  362.             self._build_cards(); win.destroy()
  363.         ctk.CTkButton(bf, text="取消", width=70, height=32, fg_color=DBTN,
  364.             hover_color=DBTN_H, text_color=TXT, font=F(12),
  365.             command=win.destroy).pack(side="right", padx=(6, 0))
  366.         ctk.CTkButton(bf, text="保存", width=70, height=32, fg_color=BLUE,
  367.             hover_color=BLUE_H, font=F(12, "bold"), command=save).pack(side="right")
  368.     # ---- 数据加载/刷新 ----
  369.     def _on_first_load(self, net, e):
  370.         if e:
  371.             self._toast("读取网络失败: " + str(e)); return
  372.         self.net = net
  373.         self._sync_adapter_menu()
  374.         if not self.config.data["profiles"]:
  375.             cur = self._cur()
  376.             if cur:
  377.                 if cur.get("dhcp"):
  378.                     self.config.data["profiles"].append(
  379.                         {"label": "自动获取(DHCP)", "dhcp": True, "ip": "", "mask": "", "gateway": "", "dns": []})
  380.                 else:
  381.                     self.config.data["profiles"].append(
  382.                         {"label": cur.get("ip") or "当前IP", "dhcp": False,
  383.                          "ip": cur.get("ip", ""), "mask": cur.get("mask") or "255.255.255.0",
  384.                          "gateway": cur.get("gateway", ""), "dns": cur.get("dns", [])})
  385.                 self.config.save()
  386.         self._build_cards()
  387.         self._toast("提示:拖到屏幕边缘可自动吸附隐藏")
  388.     def _on_refresh(self, net, e):
  389.         if e: self._toast("刷新失败"); return
  390.         self.net = net; self._sync_adapter_menu(); self._update_highlight()
  391.     def _sync_adapter_menu(self):
  392.         names = list(self.net.keys()) or ["(无适配器)"]
  393.         self.adapter_menu.configure(values=names)
  394.         if self.config.data.get("adapter") not in self.net and self.net:
  395.             self.config.data["adapter"] = next(iter(self.net))
  396.         cur = self.config.data.get("adapter") or (next(iter(self.net), "") if self.net else "")
  397.         try: self.adapter_menu.set(cur or names[0])
  398.         except Exception: pass
  399.     # ---- 尺寸自适应(每行2个, 高度精确计算, 不裁切) ----
  400.     def _fit(self):
  401.         self._rearrange()
  402.         n = len(self.config.data["profiles"])
  403.         rows = math.ceil(n / 2)            # 每行2个
  404.         h = HEADER_H + rows * ROW_STEP
  405.         x = self.root.winfo_x(); y = self.root.winfo_y()
  406.         self.root.geometry(f"{W}x{h}+{x}+{y}")
  407.     def _toast(self, msg, ms=2200):
  408.         self.toast.configure(text=f"  {msg}  ")
  409.         self.toast.place(relx=0.5, rely=1.0, anchor="s", y=-6)
  410.         self.root.after(ms, lambda: self.toast.place_forget())
  411.     # ---- 右键菜单(代替被删除的标题栏按钮) ----
  412.     def _build_menu(self):
  413.         self.menu = tk.Menu(self.root, tearoff=0, bg="#1f242b", fg=TXT,
  414.                             activebackground=BLUE, activeforeground="white",
  415.                             relief="flat", bd=0)
  416.         self.menu.add_command(label="  显示主界面  ", command=self._undock)
  417.         self.menu.add_command(label="  收起到边缘  ", command=self._dock_nearest)
  418.         self.menu.add_separator()
  419.         self.menu.add_command(label="  退出  ", command=self.root.destroy)
  420.     def _bind_rightclick(self, widget):
  421.         widget.bind("<Button-3>", self._popup)
  422.         for ch in widget.winfo_children():
  423.             self._bind_rightclick(ch)
  424.     def _popup(self, e):
  425.         try: self.menu.tk_popup(e.x_root, e.y_root)
  426.         finally: self.menu.grab_release()
  427.     # ---- 靠边吸附隐藏 ----
  428.     def _enable_dock(self): self.dock_enabled = True
  429.     def _build_dock_icon(self):
  430.         di = ctk.CTkToplevel(self.root)
  431.         di.overrideredirect(True); di.attributes("-topmost", True)
  432.         di.attributes("-alpha", 0.92); di.withdraw()
  433.         b = ctk.CTkButton(di, text="IP", fg_color=BLUE, hover_color=BLUE_H,
  434.             corner_radius=6, text_color="white", font=F(11, "bold"), command=self._undock)
  435.         b.pack(fill="both", expand=True, padx=2, pady=2)
  436.         for w in (di, b): w.bind("<Button-3>", self._popup)
  437.         self.dock_icon = di
  438.     def _geom(self, win):
  439.         return win.winfo_x(), win.winfo_y(), win.winfo_width(), win.winfo_height()
  440.     def _pointer_in(self, win):
  441.         if not win.winfo_viewable(): return False
  442.         px, py = cursor_pos(); x, y, w, h = self._geom(win)
  443.         return x <= px <= x + w and y <= py <= y + h
  444.     def _edge_side(self):
  445.         x, y, w, h = self._geom(self.root); l, t, r, b = workarea()
  446.         if x <= l + THR: return "left"
  447.         if x + w >= r - THR: return "right"
  448.         if y <= t + THR: return "top"
  449.         return None
  450.     def _dock_nearest(self):
  451.         x, y, w, h = self._geom(self.root); l, t, r, b = workarea()
  452.         cx = x + w / 2; mid = (l + r) / 2
  453.         self._dock("left" if cx < mid else "right")
  454.     def _dock(self, side):
  455.         self._dock_side = side; self._dock_geom = self._geom(self.root)
  456.         self.state = "docked"; self._away = 0
  457.         l, t, r, b = workarea(); x, y, w, h = self._dock_geom
  458.         if side == "left":   iw, ih, ix, iy = 24, 64, l, clamp(y, t, b - 64)
  459.         elif side == "right":iw, ih, ix, iy = 24, 64, r - 24, clamp(y, t, b - 64)
  460.         else:                iw, ih, ix, iy = 64, 24, clamp(x, l, r - 64), t
  461.         self.dock_icon.geometry(f"{iw}x{ih}+{ix}+{iy}")
  462.         self.dock_icon.deiconify(); self.dock_icon.lift()
  463.         self.root.withdraw()
  464.     def _undock(self):
  465.         if self.state != "docked" and self.root.winfo_viewable():
  466.             self.root.lift(); return
  467.         self.state = "shown"; self._away = 0
  468.         self.dock_icon.withdraw()
  469.         # 只恢复"位置"; 大小绝不用 winfo 记录值(含不可见边框, 会逐次叠加),
  470.         # 而是交给 _fit() 用纯算术高度重算, 与"添加方案"走同一条校正路径。
  471.         if self._dock_geom:
  472.             x, y, _w, _h = self._dock_geom      # _w,_h 故意丢弃
  473.             self.root.geometry(f"+{x}+{y}")      # 仅设位置, 不动大小
  474.             self.root.update_idletasks()
  475.         self.root.deiconify()
  476.         self.root.attributes("-topmost", True); self.root.lift()
  477.         self._fit()                              # 用算术高度 + 当前位置, 校正尺寸
  478.     def _tick(self):
  479.         try:
  480.             if self.state == "shown" and self.dock_enabled:
  481.                 side = self._edge_side()
  482.                 if side and not self._pointer_in(self.root):
  483.                     self._away += 150
  484.                     if self._away >= AWAY_MS: self._dock(side)
  485.                 else:
  486.                     self._away = 0
  487.             elif self.state == "docked":
  488.                 if self._pointer_in(self.dock_icon) or self._pointer_in(self.root):
  489.                     self._undock()
  490.         except Exception: pass
  491.         self.root.after(150, self._tick)
  492. # ---------------- 入口 ----------------
  493. if __name__ == "__main__":
  494.     if not is_admin():
  495.         run_as_admin()
  496.     ctk.set_appearance_mode("dark")      # 暗色主题, 控件自动深色
  497.     ctk.set_default_color_theme("blue")
  498.     root = ctk.CTk()
  499.     App(root)
  500.     root.mainloop()
复制代码

下载地址

https://pan.baidu.com/s/1D3K-0t3_-Aj4yqaCn_0gSQ

提取码:52pj

© 版权声明

相关文章