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

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

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

5.右击软件界面即可退出软件。
软件是用python写的,大家可直接用我打包的软件,也可以自行编译打包。已经再win10-11上测试能够正常使用。
通过网盘分享的文件:IP地址快速修改1.0
链接:
源码:
- # -*- coding: utf-8 -*-
- """ IP 快速切换工具 - 暗色半透明悬浮窗版
- 依赖: pip install customtkinter
- 无标题栏 / 默认置顶 / 靠边自动收成小图标 / 每行2个IP / 黑色半透明
- """
- import os, sys, json, threading, subprocess, ctypes, math
- import ctypes.wintypes as wt
- import tkinter as tk
- import customtkinter as ctk
-
- # ---------------- 可调常量 ----------------
- W = 300 # 窗口固定宽度
- CARD_H = 38 # 卡片高度
- ROW_STEP = 44 # 每行卡片占用高度(含间距)
- HEADER_H = 98 # 顶部固定区高度(拖动条+适配器行+标题行+底边距)
- WINDOW_ALPHA = 0.90 # 窗口半透明度(0~1, 越小越透)
- THR = 10 # 贴边判定阈值(px)
- AWAY_MS = 1200 # 鼠标离开贴边窗口多久后收起
- FONT_FAMILY = "Microsoft YaHei UI"
- CREATE_NO_WINDOW = 0x08000000
-
- # ---------------- 暗色玻璃配色 ----------------
- WIN_BG = "#14171c" # 窗口半透明黑底
- DRAG_GRIP = "#4b525c" # 拖动条抓手
- TXT = "#e8ebf0" # 主文字
- SUB = "#9aa1ab" # 次文字
- BLUE = "#3b82f6"; BLUE_H = "#2f6fe0"
- GREEN = "#22c55e"; GREEN_H = "#16a34a"
- DBTN = "#262b33"; DBTN_H = "#323842" # 深灰按钮
- DBTN_DEL_H = "#3a2226" # 删除按钮hover
- RED = "#ff6b6b"
- CARD = "#20242b"; CARD_B = "#333a44"
- CARD_SEL = "#262c36"; CARD_SEL_B = "#5b8def"
- CARD_ACT = "#1d2942"; CARD_ACT_B = "#3b82f6"
- DOT = "#5a626d"
- TOAST_BG = "#2563eb"
-
- def F(size, weight="normal"):
- return ctk.CTkFont(family=FONT_FAMILY, size=size, weight=weight)
-
- # ---------------- 系统工具 ----------------
- def is_admin():
- try: return bool(ctypes.windll.shell32.IsUserAnAdmin())
- except Exception: return False
-
- def run_as_admin():
- if getattr(sys, "frozen", False):
- exe, params = sys.executable, subprocess.list2cmdline(sys.argv[1:])
- else:
- exe = sys.executable.replace("python.exe", "pythonw.exe")
- if not os.path.exists(exe): exe = sys.executable
- params = subprocess.list2cmdline(sys.argv)
- ctypes.windll.shell32.ShellExecuteW(None, "runas", exe, params, None, 1)
- sys.exit(0)
-
- def cursor_pos():
- p = wt.POINT(); ctypes.windll.user32.GetCursorPos(ctypes.byref(p)); return p.x, p.y
-
- def workarea():
- r = wt.RECT(); ctypes.windll.user32.SystemParametersInfoW(0x0030, 0, ctypes.byref(r), 0)
- return r.left, r.top, r.right, r.bottom
-
- def clamp(v, lo, hi): return max(lo, min(hi, v))
-
- # ---------------- 网络操作 ----------------
- PS_SCRIPT = r"""
- [Console]::OutputEncoding=[Text.UTF8Encoding]::new()
- function P2M($p){
- if($p -le 0){return '0.0.0.0'}; if($p -ge 32){return '255.255.255.255'}
- $m=[uint32](((1 -shl 32) - 1) -shl (32-$p))
- return ("{0}.{1}.{2}.{3}" -f (($m -shr 24) -band 255),(($m -shr 16) -band 255),(($m -shr 8) -band 255),($m -band 255))
- }
- $out=@()
- Get-NetAdapter | Where-Object {$_.HardwareInterface -eq $true -and $_.Status -ne 'Disabled'} | ForEach-Object {
- $n=$_.Name
- $ipObj=Get-NetIPAddress -InterfaceAlias $n -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1
- $gw=(Get-NetRoute -InterfaceAlias $n -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Select-Object -First 1).NextHop
- $dns=(Get-DnsClientServerAddress -InterfaceAlias $n -AddressFamily IPv4 -ErrorAction SilentlyContinue).ServerAddresses
- if($null -eq $dns){$dns=@()}else{$dns=@($dns)}
- $dhcp = if($ipObj){$ipObj.PrefixOrigin -eq 'Dhcp'}else{$false}
- $ip = if($ipObj){$ipObj.IPAddress}else{''}
- $mask = if($ipObj){P2M $ipObj.PrefixLength}else{''}
- $out += [ordered]@{name=$n; ip=$ip; mask=$mask; gateway=$gw; dns=$dns; dhcp=$dhcp}
- }
- [pscustomobject]@{adapters=$out} | ConvertTo-Json -Depth 5
- """
-
- class NetManager:
- def _run(self, args):
- r = subprocess.run(args, creationflags=CREATE_NO_WINDOW,
- capture_output=True, timeout=20)
- if r.returncode != 0:
- err = (r.stderr or b"").decode("gbk", "ignore").strip()
- raise RuntimeError(err or f"命令失败 code={r.returncode}")
- return r
-
- def get_all(self):
- r = self._run(["powershell", "-NoProfile", "-Command", PS_SCRIPT])
- data = json.loads(r.stdout.decode("utf-8", "ignore") or "{}")
- ad = data.get("adapters") or []
- if isinstance(ad, dict): ad =
- net = {}
- for a in ad:
- net[a.get("name")] = {
- "ip": a.get("ip") or "", "mask": a.get("mask") or "",
- "gateway": a.get("gateway") or "", "dns": a.get("dns") or [],
- "dhcp": bool(a.get("dhcp")),
- }
- return net
-
- def apply(self, adapter, p):
- n = f"name={adapter}"
- if p.get("dhcp"):
- self._run(["netsh", "interface", "ip", "set", "address", n, "dhcp"])
- self._run(["netsh", "interface", "ip", "set", "dns", n, "dhcp"])
- return
- ip = (p.get("ip") or "").strip()
- if not ip: raise RuntimeError("静态方案缺少 IP")
- mask = (p.get("mask") or "255.255.255.0").strip()
- gw = (p.get("gateway") or "").strip()
- cmd = ["netsh", "interface", "ip", "set", "address", n, "static", ip, mask]
- if gw: cmd += [gw, "1"]
- self._run(cmd)
- dns = [d.strip() for d in (p.get("dns") or []) if d.strip()]
- if dns:
- self._run(["netsh", "interface", "ip", "set", "dns", n, "static", dns[0]])
- for d in dns[1:]:
- self._run(["netsh", "interface", "ip", "add", "dns", n, f"addr={d}"])
- else:
- self._run(["netsh", "interface", "ip", "set", "dns", n, "dhcp"])
-
- # ---------------- 配置存储 ----------------
- def config_path():
- base = os.path.dirname(sys.executable if getattr(sys, "frozen", False)
- else os.path.abspath(__file__))
- p = os.path.join(base, "ip_switcher_config.json")
- try:
- with open(p, "a"): pass
- return p
- except Exception:
- d = os.path.join(os.environ.get("APPDATA", base), "IPSwitcher")
- os.makedirs(d, exist_ok=True)
- return os.path.join(d, "ip_switcher_config.json")
-
- class Config:
- def __init__(self):
- self.path = config_path()
- self.data = {"adapter": "", "profiles": []}
- try:
- with open(self.path, "r", encoding="utf-8") as f:
- self.data.update(json.load(f))
- except Exception: pass
- self.data.setdefault("profiles", [])
-
- def save(self):
- try:
- with open(self.path, "w", encoding="utf-8") as f:
- json.dump(self.data, f, ensure_ascii=False, indent=2)
- except Exception: pass
-
- # ---------------- 主界面 ----------------
- class App:
- def __init__(self, root):
- self.root = root
- self.netmgr = NetManager()
- self.config = Config()
- self.net = {}
- self.selected = None
- self.card_widgets = []
- self.state = "shown"
- self.dock_enabled = False
- self._away = 0
- self._dock_side = None
- self._dock_geom = None
- self._drag = None
-
- l, t, r, b = workarea()
- self.root.title("IPSwitcher")
- self.root.overrideredirect(True)
- self.root.configure(fg_color=WIN_BG)
- self.root.geometry(f"{W}x140+{r - W - 60}+{t + 40}")
- self.root.attributes("-topmost", True) # 默认置顶
- self.root.attributes("-alpha", WINDOW_ALPHA) # 黑色半透明
-
- self._build_dragbar()
- self._build_body()
- self._build_dock_icon()
- self._build_menu()
- self._bind_rightclick(self.root) # 全窗右键菜单
-
- self.root.after(150, self._tick)
- self.root.after(3000, self._enable_dock)
- self._run_async(self.netmgr.get_all, self._on_first_load)
-
- # ---- 异步执行 ----
- def _run_async(self, fn, on_done):
- def w():
- try: r, e = fn(), None
- except Exception as ex: r, e = None, ex
- self.root.after(0, lambda: on_done(r, e))
- threading.Thread(target=w, daemon=True).start()
-
- # ---- 透明拖动条(无按钮/无文字, 仅抓手) ----
- def _build_dragbar(self):
- db = ctk.CTkFrame(self.root, height=16, corner_radius=0, fg_color="transparent")
- db.pack(fill="x"); db.pack_propagate(False)
- grip = ctk.CTkFrame(db, width=34, height=4, corner_radius=2, fg_color=DRAG_GRIP)
- grip.pack(expand=True)
- for w in (db, grip):
- w.bind("<Button-1>", self._drag_start)
- w.bind("<B1-Motion>", self._drag_move)
- w.bind("<Enter>", lambda e: self.root.configure(cursor="fleur"))
- w.bind("<Leave>", lambda e: self.root.configure(cursor=""))
-
- def _drag_start(self, e):
- self._drag = (e.x_root, e.y_root, self.root.winfo_x(), self.root.winfo_y())
- def _drag_move(self, e):
- if not self._drag: return
- x0, y0, wx, wy = self._drag
- self.root.geometry(f"+{wx + e.x_root - x0}+{wy + e.y_root - y0}")
-
- # ---- 主体 ----
- def _build_body(self):
- body = ctk.CTkFrame(self.root, fg_color="transparent", corner_radius=0)
- body.pack(fill="x")
-
- f1 = ctk.CTkFrame(body, fg_color="transparent"); f1.pack(fill="x", padx=10, pady=(8, 4))
- self.adapter_menu = ctk.CTkOptionMenu(f1, values=["(无适配器)"],
- command=self._on_adapter_change, width=120, height=30, font=F(12),
- dropdown_font=F(12))
- self.adapter_menu.pack(side="left", fill="x", expand=True, padx=(0, 6))
- ctk.CTkButton(f1, text="↻", width=34, height=30, fg_color=DBTN,
- hover_color=DBTN_H, text_color=TXT, font=F(15),
- command=self._on_refresh_click).pack(side="left", padx=(0, 6))
- ctk.CTkButton(f1, text="应用", width=56, height=30, fg_color=BLUE,
- hover_color=BLUE_H, font=F(12, "bold"),
- command=self._on_apply_selected).pack(side="left")
-
- f2 = ctk.CTkFrame(body, fg_color="transparent"); f2.pack(fill="x", padx=10, pady=(4, 4))
- ctk.CTkLabel(f2, text="IP 方案 双击切换", text_color=SUB,
- font=F(11)).pack(side="left")
- ctk.CTkButton(f2, text="删", width=30, height=24, fg_color=DBTN,
- hover_color=DBTN_DEL_H, text_color=RED, font=F(11),
- command=self._on_delete).pack(side="right", padx=(2, 0))
- ctk.CTkButton(f2, text="改", width=30, height=24, fg_color=DBTN,
- hover_color=DBTN_H, text_color=TXT, font=F(11),
- command=self._on_edit).pack(side="right", padx=(2, 0))
- ctk.CTkButton(f2, text="+", width=30, height=24, fg_color=GREEN,
- hover_color=GREEN_H, text_color="white", font=F(13, "bold"),
- command=self._on_add).pack(side="right")
-
- self.card_container = ctk.CTkFrame(body, fg_color="transparent")
- self.card_container.pack(fill="x", padx=8, pady=(0, 8))
- self.card_container.columnconfigure(0, weight=1) # 固定2列, 宽度自适应
- self.card_container.columnconfigure(1, weight=1)
-
- self.toast = ctk.CTkLabel(self.root, text="", fg_color=TOAST_BG,
- text_color="white", corner_radius=6, font=F(11))
-
- # ---- 卡片(只显示IP, 宽度自适应) ----
- def _build_cards(self):
- for w in self.card_container.winfo_children(): w.destroy()
- self.card_widgets = []
- for i, p in enumerate(self.config.data["profiles"]):
- self.card_widgets.append(self._make_card(i, p))
- self._fit()
- self._update_highlight()
-
- def _make_card(self, i, p):
- card = ctk.CTkFrame(self.card_container, height=CARD_H,
- corner_radius=8, border_width=1, fg_color=CARD, border_color=CARD_B)
- card.pack_propagate(False)
- inner = ctk.CTkFrame(card, fg_color="transparent")
- inner.pack(expand=True, fill="x", padx=8)
- dot = ctk.CTkLabel(inner, text="●", text_color=DOT, font=F(10), width=14)
- dot.pack(side="left")
- lab = ctk.CTkLabel(inner, text=p.get("label", ""), text_color=TXT,
- font=F(12, "bold"), anchor="w")
- lab.pack(side="left", padx=(2, 0))
- for w in (card, inner, dot, lab):
- w.bind("<Button-1>", lambda e, i=i: self._select(i))
- w.bind("<Double-Button-1>", lambda e, i=i: self._apply_index(i))
- card._index, card._dot, card._lab = i, dot, lab
- return card
-
- def _rearrange(self):
- for i, card in enumerate(self.card_widgets): # 固定每行2个
- r, c = divmod(i, 2)
- card.grid(row=r, column=c, sticky="ew", padx=3, pady=3)
- self.card_container.update_idletasks()
-
- def _cur(self):
- return self.net.get(self._cur_adapter())
-
- def _cur_adapter(self):
- a = self.config.data.get("adapter")
- if a in self.net: return a
- return next(iter(self.net), "")
-
- def _match(self, p):
- cur = self._cur()
- if not cur: return False
- if p.get("dhcp"): return bool(cur.get("dhcp"))
- return (not cur.get("dhcp")) and cur.get("ip") == p.get("ip")
-
- def _update_highlight(self):
- for card in self.card_widgets:
- p = self.config.data["profiles"][card._index]
- act = self._match(p); sel = (card._index == self.selected)
- if act:
- card.configure(fg_color=CARD_ACT, border_color=CARD_ACT_B)
- card._dot.configure(text_color=BLUE)
- elif sel:
- card.configure(fg_color=CARD_SEL, border_color=CARD_SEL_B)
- card._dot.configure(text_color=DOT)
- else:
- card.configure(fg_color=CARD, border_color=CARD_B)
- card._dot.configure(text_color=DOT)
-
- def _select(self, i):
- self.selected = i; self._update_highlight()
-
- def _apply_index(self, i):
- self.selected = i
- self._do_apply(self.config.data["profiles"][i])
-
- def _do_apply(self, p):
- adapter = self._cur_adapter()
- if not adapter:
- self._toast("没有可用适配器"); return
- self._toast("应用中…")
- self._run_async(lambda: self.netmgr.apply(adapter, p),
- lambda r, e: (self._toast("已应用 ✔" if not e else "失败: " + str(e)),
- self._run_async(self.netmgr.get_all, self._on_refresh)))
-
- # ---- 顶部按钮回调 ----
- def _on_adapter_change(self, name):
- if name in ("(无适配器)",): return
- self.config.data["adapter"] = name; self.config.save()
- self._update_highlight()
-
- def _on_refresh_click(self):
- self._toast("刷新中…")
- self._run_async(self.netmgr.get_all, self._on_refresh)
-
- def _on_apply_selected(self):
- if self.selected is None:
- self._toast("请先单击/双击选中一个方案"); return
- self._do_apply(self.config.data["profiles"][self.selected])
-
- def _on_add(self): self._edit_dialog(None)
- def _on_edit(self):
- if self.selected is None: self._toast("先选中一个方案"); return
- self._edit_dialog(self.selected)
- def _on_delete(self):
- if self.selected is None: self._toast("先选中一个方案"); return
- self.config.data["profiles"].pop(self.selected)
- self.selected = None; self.config.save(); self._build_cards()
-
- # ---- 编辑弹窗 ----
- def _edit_dialog(self, index):
- editing = index is not None
- p = dict(self.config.data["profiles"][index]) if editing else \
- {"dhcp": False, "ip": "", "mask": "255.255.255.0", "gateway": "", "dns": []}
- win = ctk.CTkToplevel(self.root)
- win.title("编辑方案" if editing else "新增方案")
- win.attributes("-topmost", True); win.transient(self.root)
- win.geometry(f"320x280+{self.root.winfo_x()}+{self.root.winfo_y() + 30}")
- win.grab_set()
-
- var = tk.StringVar(value="on" if p.get("dhcp") else "off")
- rows = []
- def row(label, key, placeholder=""):
- f = ctk.CTkFrame(win, fg_color="transparent"); f.pack(fill="x", padx=14, pady=3)
- ctk.CTkLabel(f, text=label, width=46, anchor="w", font=F(12)).pack(side="left")
- e = ctk.CTkEntry(f, height=30, font=F(12), placeholder_text=placeholder)
- e.pack(side="left", fill="x", expand=True)
- e.insert(0, str(p.get(key, "") or ""))
- rows.append((key, e)); return e
-
- cf = ctk.CTkFrame(win, fg_color="transparent"); cf.pack(fill="x", padx=14, pady=(12, 6))
- cb = ctk.CTkCheckBox(cf, text="自动获取 IP (DHCP)", variable=var,
- onvalue="on", offvalue="off", font=F(12), command=lambda: toggle())
- cb.pack(side="left")
-
- e_ip = row("IP", "ip", "192.168.1.100")
- e_mask = row("掩码", "mask", "255.255.255.0")
- e_gw = row("网关", "gateway", "192.168.1.1")
- e_dns = row("DNS", "dns", "8.8.8.8, 114.114.114.114")
-
- def toggle():
- st = "disabled" if var.get() == "on" else "normal"
- for _, e in rows: e.configure(state=st)
- toggle()
-
- bf = ctk.CTkFrame(win, fg_color="transparent"); bf.pack(fill="x", padx=14, pady=12)
- def save():
- dhcp = var.get() == "on"
- ip = e_ip.get().strip(); mask = e_mask.get().strip() or "255.255.255.0"
- gw = e_gw.get().strip()
- dns = [d.strip() for d in e_dns.get().replace(";", ",").replace(";", ",").split(",") if d.strip()]
- if not dhcp and not ip:
- self._toast("静态方案必须填写 IP"); return
- label = "自动获取(DHCP)" if dhcp else (ip or "未设置IP")
- new = {"label": label, "dhcp": dhcp, "ip": ip, "mask": mask, "gateway": gw, "dns": dns}
- if editing: self.config.data["profiles"][index] = new; si = index
- else: self.config.data["profiles"].append(new); si = len(self.config.data["profiles"]) - 1
- self.config.save(); self.selected = si
- self._build_cards(); win.destroy()
- ctk.CTkButton(bf, text="取消", width=70, height=32, fg_color=DBTN,
- hover_color=DBTN_H, text_color=TXT, font=F(12),
- command=win.destroy).pack(side="right", padx=(6, 0))
- ctk.CTkButton(bf, text="保存", width=70, height=32, fg_color=BLUE,
- hover_color=BLUE_H, font=F(12, "bold"), command=save).pack(side="right")
-
- # ---- 数据加载/刷新 ----
- def _on_first_load(self, net, e):
- if e:
- self._toast("读取网络失败: " + str(e)); return
- self.net = net
- self._sync_adapter_menu()
- if not self.config.data["profiles"]:
- cur = self._cur()
- if cur:
- if cur.get("dhcp"):
- self.config.data["profiles"].append(
- {"label": "自动获取(DHCP)", "dhcp": True, "ip": "", "mask": "", "gateway": "", "dns": []})
- else:
- self.config.data["profiles"].append(
- {"label": cur.get("ip") or "当前IP", "dhcp": False,
- "ip": cur.get("ip", ""), "mask": cur.get("mask") or "255.255.255.0",
- "gateway": cur.get("gateway", ""), "dns": cur.get("dns", [])})
- self.config.save()
- self._build_cards()
- self._toast("提示:拖到屏幕边缘可自动吸附隐藏")
-
- def _on_refresh(self, net, e):
- if e: self._toast("刷新失败"); return
- self.net = net; self._sync_adapter_menu(); self._update_highlight()
-
- def _sync_adapter_menu(self):
- names = list(self.net.keys()) or ["(无适配器)"]
- self.adapter_menu.configure(values=names)
- if self.config.data.get("adapter") not in self.net and self.net:
- self.config.data["adapter"] = next(iter(self.net))
- cur = self.config.data.get("adapter") or (next(iter(self.net), "") if self.net else "")
- try: self.adapter_menu.set(cur or names[0])
- except Exception: pass
-
- # ---- 尺寸自适应(每行2个, 高度精确计算, 不裁切) ----
- def _fit(self):
- self._rearrange()
- n = len(self.config.data["profiles"])
- rows = math.ceil(n / 2) # 每行2个
- h = HEADER_H + rows * ROW_STEP
- x = self.root.winfo_x(); y = self.root.winfo_y()
- self.root.geometry(f"{W}x{h}+{x}+{y}")
-
- def _toast(self, msg, ms=2200):
- self.toast.configure(text=f" {msg} ")
- self.toast.place(relx=0.5, rely=1.0, anchor="s", y=-6)
- self.root.after(ms, lambda: self.toast.place_forget())
-
- # ---- 右键菜单(代替被删除的标题栏按钮) ----
- def _build_menu(self):
- self.menu = tk.Menu(self.root, tearoff=0, bg="#1f242b", fg=TXT,
- activebackground=BLUE, activeforeground="white",
- relief="flat", bd=0)
- self.menu.add_command(label=" 显示主界面 ", command=self._undock)
- self.menu.add_command(label=" 收起到边缘 ", command=self._dock_nearest)
- self.menu.add_separator()
- self.menu.add_command(label=" 退出 ", command=self.root.destroy)
-
- def _bind_rightclick(self, widget):
- widget.bind("<Button-3>", self._popup)
- for ch in widget.winfo_children():
- self._bind_rightclick(ch)
-
- def _popup(self, e):
- try: self.menu.tk_popup(e.x_root, e.y_root)
- finally: self.menu.grab_release()
-
- # ---- 靠边吸附隐藏 ----
- def _enable_dock(self): self.dock_enabled = True
-
- def _build_dock_icon(self):
- di = ctk.CTkToplevel(self.root)
- di.overrideredirect(True); di.attributes("-topmost", True)
- di.attributes("-alpha", 0.92); di.withdraw()
- b = ctk.CTkButton(di, text="IP", fg_color=BLUE, hover_color=BLUE_H,
- corner_radius=6, text_color="white", font=F(11, "bold"), command=self._undock)
- b.pack(fill="both", expand=True, padx=2, pady=2)
- for w in (di, b): w.bind("<Button-3>", self._popup)
- self.dock_icon = di
-
- def _geom(self, win):
- return win.winfo_x(), win.winfo_y(), win.winfo_width(), win.winfo_height()
-
- def _pointer_in(self, win):
- if not win.winfo_viewable(): return False
- px, py = cursor_pos(); x, y, w, h = self._geom(win)
- return x <= px <= x + w and y <= py <= y + h
-
- def _edge_side(self):
- x, y, w, h = self._geom(self.root); l, t, r, b = workarea()
- if x <= l + THR: return "left"
- if x + w >= r - THR: return "right"
- if y <= t + THR: return "top"
- return None
-
- def _dock_nearest(self):
- x, y, w, h = self._geom(self.root); l, t, r, b = workarea()
- cx = x + w / 2; mid = (l + r) / 2
- self._dock("left" if cx < mid else "right")
-
- def _dock(self, side):
- self._dock_side = side; self._dock_geom = self._geom(self.root)
- self.state = "docked"; self._away = 0
- l, t, r, b = workarea(); x, y, w, h = self._dock_geom
- if side == "left": iw, ih, ix, iy = 24, 64, l, clamp(y, t, b - 64)
- elif side == "right":iw, ih, ix, iy = 24, 64, r - 24, clamp(y, t, b - 64)
- else: iw, ih, ix, iy = 64, 24, clamp(x, l, r - 64), t
- self.dock_icon.geometry(f"{iw}x{ih}+{ix}+{iy}")
- self.dock_icon.deiconify(); self.dock_icon.lift()
- self.root.withdraw()
-
- def _undock(self):
- if self.state != "docked" and self.root.winfo_viewable():
- self.root.lift(); return
- self.state = "shown"; self._away = 0
- self.dock_icon.withdraw()
- # 只恢复"位置"; 大小绝不用 winfo 记录值(含不可见边框, 会逐次叠加),
- # 而是交给 _fit() 用纯算术高度重算, 与"添加方案"走同一条校正路径。
- if self._dock_geom:
- x, y, _w, _h = self._dock_geom # _w,_h 故意丢弃
- self.root.geometry(f"+{x}+{y}") # 仅设位置, 不动大小
- self.root.update_idletasks()
- self.root.deiconify()
- self.root.attributes("-topmost", True); self.root.lift()
- self._fit() # 用算术高度 + 当前位置, 校正尺寸
-
- def _tick(self):
- try:
- if self.state == "shown" and self.dock_enabled:
- side = self._edge_side()
- if side and not self._pointer_in(self.root):
- self._away += 150
- if self._away >= AWAY_MS: self._dock(side)
- else:
- self._away = 0
- elif self.state == "docked":
- if self._pointer_in(self.dock_icon) or self._pointer_in(self.root):
- self._undock()
- except Exception: pass
- self.root.after(150, self._tick)
-
- # ---------------- 入口 ----------------
- if __name__ == "__main__":
- if not is_admin():
- run_as_admin()
- ctk.set_appearance_mode("dark") # 暗色主题, 控件自动深色
- ctk.set_default_color_theme("blue")
- root = ctk.CTk()
- App(root)
- root.mainloop()
复制代码
下载地址
https://pan.baidu.com/s/1D3K-0t3_-Aj4yqaCn_0gSQ
提取码:52pj
© 版权声明
文章版权归作者所有,未经允许请勿转载。
