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

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

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

5. 右击软件界面即可退出软件。
软件是用 python 写的,大家可直接用我打包的软件,也可以自行编译打包。已经再 win10-11 上测试能够正常使用。
通过网盘分享的文件:IP 地址快速修改 1.0
链接: https://pan.baidu.com/s/1D3K-0t3_-Aj4yqaCn_0gSQ 提取码: 52pj
源码:
# -*- 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 = [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("", self._drag_start)
w.bind("", self._drag_move)
w.bind("", lambda e: self.root.configure(cursor="fleur"))
w.bind("", 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("", lambda e, i=i: self._select(i))
w.bind("", 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("", 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("", 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()
正文完
