电脑IP地址快速切换

共计 18214 个字符,预计需要花费 46 分钟才能阅读完成。

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

正文完
 0
suyan
版权声明:本站原创文章,由 suyan 于2026-07-26发表,共计18214字。
转载说明:转载本网站任何内容,请按照转载方式正确书写本站原文地址。本站提供的一切软件、教程和内容信息仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件QQ邮箱:suyanw520@163.com 与我们联系处理。敬请谅解!
评论(没有评论)
验证码