用Python写一个自动跟随Codex窗口的悬浮小工具 📅 发布时间:2026/8/31 9:18:33 👁 浏览次数: 平时用 Codex 做开发助手时我习惯把它独立放在一个窗口里旁边再开一个草稿窗口记录思路、保存临时代码片段。这样用确实顺手但有一个很实际的痛点一旦把 Codex 窗口拖到另一块屏幕或者切换了桌面布局旁边的草稿窗口不会跟着走。每次都要手动把两个窗口重新摆到一起遇到正在排查问题的场景特别打断思路。于是我用 Python 写了一个小工具一个无边框悬浮小窗会自动“贴”在 Codex 窗口右侧。Codex 窗口移动到哪它就跟着移动到哪Codex 最小化它就自动隐藏Codex 恢复它再自动出现。这篇文章就把完整的实现思路、代码、常见问题和工程化建议分享出来。无论你使用的是 Codex CLI、Codex 桌面端还是把 Codex 跑在 VS Code 终端里这个方案都适用。1. 为什么要做“跟随窗口”Codex 这类 AI 编程助手实际使用时通常不是单独一个软件而是有多种使用形态。有些开发者喜欢用 Codex CLI直接在终端里对话有些用桌面客户端还有一些会在 VS Code 里安装 Codex 插件作为侧边栏工具使用。窗口形态不同但共同需求是一样的Codex 内容区旁边往往需要另一个辅助窗口来放上下文、临时笔记、生成的代码片段或者用来展示任务状态。如果不做任何处理辅助窗口和主窗口之间的位置关系是“静态”的。你第一次把它们摆放好可能只维持到下次拖动窗口之前。一旦移动了 Codex 窗口辅助窗口还留在原地看起来就会很散乱。多显示器环境下问题更明显副屏上的 Codex 窗口和主屏上的笔记窗口经常隔得很远。“跟随窗口”解决的就是这个动态布局问题。它本质上是一个位置同步工具监听目标窗口的位置变化然后实时调整辅助窗口的坐标。用户只需要用一次鼠标把辅助小窗拖到目标窗口旁边之后无论怎么移动 Codex辅助小窗都会保持相对位置不需要再手动整理。这个方案听起来很“玩具”但在日常开发里非常实用尤其是需要长时间盯着 Codex 输出的时候。辅助窗口可以展示命令执行状态、记录问答历史、放参考文档甚至做成一个简单的“复制板”。下面我们开始拆解具体实现。2. 窗口跟随的核心原理在 Windows 系统中每个可见应用的顶层窗口都有一个句柄HWND。操作系统通过句柄管理窗口的位置、大小、可见性、层级等属性。要实现窗口跟随关键就是两件事第一拿到目标窗口的句柄。Codex 窗口可能是桌面客户端的顶层窗口也可能是终端窗口或者 VS Code 主窗口。无论哪种情况只要有句柄就能读取到它的屏幕坐标。第二读取目标窗口的位置和大小设置辅助窗口的位置。Windows 提供了GetWindowRect这样的 API可以拿到窗口左上角和右下角的屏幕坐标再通过SetWindowPos、MoveWindow或者直接设置 Tkinter 窗口的geometry就能把辅助窗口移动到目标窗口旁边。需要强调的是“跟随”并不等同于“置顶”。置顶是让窗口始终浮在其他窗口上方不改变位置而跟随是让辅助窗口的位置随着目标窗口变化。当然实际开发中两者经常一起使用辅助窗口既要贴住 Codex又要保持浮在最上层这样无论切到什么应用旁边的小窗都能看到。实现方式上最常见的是“轮询”方案每隔 200 到 300 毫秒读取一次目标窗口位置如果位置变化超过阈值就更新辅助窗口坐标。这种方式简单、稳定而且足够应付日常使用。更高级的方案是监听窗口事件比如使用WinEventHook订阅窗口移动事件但代码复杂度会高很多。对于这个场景轮询已经足够。3. 环境准备与版本说明本文示例代码使用 Python 和 pywin32 库主要运行环境是 Windows 10 或 Windows 11。你需要在机器上安装Python 3.8 或更高版本。pywin32用于调用 Windows API。psutil如果希望按进程名匹配窗口这个库是可选的。安装命令如下pip install pywin32 psutil如果你的环境已经安装了pywin32但命令行找不到win32gui模块可以检查一下 Python 版本是否与 pywin32 版本匹配或者重新执行一次安装pip install --upgrade pywin32本文示例中的界面部分使用 Python 自带的 Tkinter不需要额外安装。Tkinter 是 Python 标准库的一部分正常情况下安装 Python 时就会带上。如果你运行import tkinter报错说明当前 Python 发行版没有包含 Tk 支持需要重新安装带 Tk 的 Python 版本。版本相关说明Codex 产品形态更新较快实际窗口标题可能随版本变化。本文示例不会把窗口标题写死而是提供了一个关键字参数运行命令时动态传入。如果你的 Codex 窗口标题不叫 “Codex”只需要换一个关键字即可。示例项目结构如下codex-follower/ ├── main.py ├── find_window.py ├── rect_util.py └── follower.py其中find_window.py负责查找目标窗口句柄rect_util.py负责读取窗口坐标和处理 DPI 缩放follower.py是核心的跟随窗口界面main.py是启动入口。4. 核心代码实现下面按文件拆分每一步都会说明代码的作用。4.1 查找 Codex 窗口句柄查找窗口句柄有两种常见方式按窗口标题关键字匹配或者按进程名匹配。先来看最稳妥的标题匹配方式。你可以在运行工具时传入--title Codex程序会在所有可见顶层窗口中搜索标题包含 “Codex” 的窗口。为了避免匹配到不相关的窗口还可以同时指定进程名。# find_window.py import win32gui import win32process import psutil class WindowFinder: 按标题或进程名查找目标窗口句柄。 def __init__(self, title_keywordNone, process_nameNone): self.title_keyword title_keyword self.process_name process_name self.hwnd None def _match(self, hwnd): if not win32gui.IsWindowVisible(hwnd): return False if self.title_keyword: title win32gui.GetWindowText(hwnd) if self.title_keyword.lower() in title.lower(): return True if self.process_name: try: _, pid win32process.GetWindowThreadProcessId(hwnd) proc_name psutil.Process(pid).name().lower() except Exception: proc_name if proc_name self.process_name.lower(): return True return False def find(self): self.hwnd None def enum_proc(hwnd, lparam): if self._match(hwnd): self.hwnd hwnd return False return True win32gui.EnumWindows(enum_proc, None) return self.hwndwin32gui.EnumWindows会枚举当前系统所有顶层窗口回调函数返回True表示继续遍历返回False表示停止。IsWindowVisible用来过滤被隐藏的窗口避免匹配到系统内部窗口。这里有个细节需要关注如果 Codex 是跑在终端里窗口标题可能是终端标题而不是 “Codex”。比如你在 Windows Terminal 里运行codex命令窗口句柄属于 Windows Terminal不属于 Codex 进程。这种情况下建议用终端窗口的标题关键字或者在终端标题里加上 “Codex” 字样。4.2 读取窗口坐标与 DPI 处理拿到窗口句柄后可以用GetWindowRect读取窗口的屏幕坐标。这个函数返回四个值左边界、上边界、右边界、下边界。用右边界减去左边界就是窗口宽度下边界减去上边界就是窗口高度。高分屏环境下需要额外注意 DPI 缩放。Windows 默认会对没有声明 DPI 感知的程序进行坐标缩放。如果你的 Python 程序没有调用 DPI 感知函数GetWindowRect拿到的坐标可能和实际屏幕坐标不一致导致辅助窗口位置偏到错误的地方。# rect_util.py import ctypes import win32gui class Rect: def __init__(self, left, top, right, bottom): self.left left self.top top self.right right self.bottom bottom property def width(self): return self.right - self.left property def height(self): return self.bottom - self.top def get_window_rect(hwnd): left, top, right, bottom win32gui.GetWindowRect(hwnd) return Rect(left, top, right, bottom) def set_dpi_awareness(): try: ctypes.windll.shcore.SetProcessDpiAwareness(2) except Exception: try: ctypes.windll.user32.SetProcessDPIAware() except Exception: passSetProcessDpiAwareness(2)表示按每个显示器的 DPI 感知模式运行适用于多显示器且缩放比例不同的场景。如果系统版本较老不支持这个接口就回退到SetProcessDPIAware。这段代码应该在创建任何窗口之前调用否则 Tkinter 窗口可能已经按缩放后的虚拟坐标初始化后续坐标换算会不一致。4.3 实现跟随窗口界面接下来是核心的follower.py。这个文件里使用 Tkinter 创建了一个无边框置顶小窗然后通过after定时器循环读取 Codex 窗口位置并移动小窗。选择after而不是threading是因为 Tkinter 的界面操作必须在主线程中完成。如果你在子线程里直接修改 Tkinter 控件轻则界面卡顿重则崩溃。after是 Tkinter 官方推荐的定时任务方式既不会阻塞界面又能安全地访问控件。# follower.py import tkinter as tk import win32gui from find_window import WindowFinder from rect_util import get_window_rect class FollowWindow: def __init__( self, finder, width240, height180, offset_x20, offset_y0, interval_ms300, ): self.finder finder self.width width self.height height self.offset_x offset_x self.offset_y offset_y self.interval_ms interval_ms self.running True self.paused False self.last_pos None self.hidden False self.root tk.Tk() self.root.title(跟随小窗) self.root.overrideredirect(True) self.root.attributes(-topmost, True) self.root.configure(bg#2b2b2b) self._build_ui() self.finder.find() def _build_ui(self): self.title_label tk.Label( self.root, text跟随中, fg#ffffff, bg#2b2b2b, font(Microsoft YaHei, 12), ) self.title_label.pack(pady10) tip_label tk.Label( self.root, text拖动 Codex 窗口我会自动跟在旁边, fg#aaaaaa, bg#2b2b2b, font(Microsoft YaHei, 9), ) tip_label.pack(padx10) btn_frame tk.Frame(self.root, bg#2b2b2b) btn_frame.pack(pady8) self.pause_btn tk.Button( btn_frame, text暂停, commandself.toggle_pause, width6 ) self.pause_btn.grid(row0, column0, padx5) close_btn tk.Button( btn_frame, text退出, commandself.stop, width6 ) close_btn.grid(row0, column1, padx5) def toggle_pause(self): self.paused not self.paused self.pause_btn.config(text继续 if self.paused else 暂停) self.title_label.config(text已暂停 if self.paused else 跟随中) def _update_position(self): if not self.running: return if not self.paused: hwnd self.finder.hwnd if hwnd is None or not win32gui.IsWindow(hwnd): hwnd self.finder.find() if hwnd: if win32gui.IsIconic(hwnd): if not self.hidden: self.root.withdraw() self.hidden True else: rect get_window_rect(hwnd) target_x rect.left rect.width self.offset_x target_y rect.top self.offset_y if ( self.last_pos is None or abs(target_x - self.last_pos[0]) 2 or abs(target_y - self.last_pos[1]) 2 ): self.root.geometry( f{self.width}x{self.height}{target_x}{target_y} ) self.last_pos (target_x, target_y) if self.hidden or not self.root.winfo_viewable(): self.root.deiconify() self.hidden False else: if not self.hidden: self.root.withdraw() self.hidden True self.last_pos None self.root.after(self.interval_ms, self._update_position) def run(self): self.root.after(self.interval_ms, self._update_position) self.root.mainloop() def stop(self): self.running False self.root.destroy()代码里的跟随逻辑可以和实际场景对应起来。第一目标窗口最小化时IsIconic返回True。这时候如果继续读取窗口坐标拿到的可能是最小化状态的坐标不适合用来计算跟随位置所以直接隐藏辅助窗口。第二目标窗口恢复后辅助窗口需要重新显示。这里通过self.hidden状态和root.winfo_viewable()判断避免反复调用deiconify造成闪烁。第三坐标更新加了 2 像素的阈值。如果 Codex 窗口只是原地不动或轻微抖动辅助窗口不会频繁调用geometry减少界面刷新压力。4.4 启动入口main.py负责接收命令行参数初始化查找器和跟随窗口。通过命令行传参可以让工具不依赖写死的窗口标题适应不同使用方式。# main.py import argparse from find_window import WindowFinder from follower import FollowWindow from rect_util import set_dpi_awareness def parse_args(): parser argparse.ArgumentParser(descriptionCodex 跟随窗口小工具) parser.add_argument(--title, defaultCodex, help窗口标题关键字) parser.add_argument(--process, defaultNone, help进程名例如 codex.exe) parser.add_argument(--width, typeint, default240, help小窗宽度) parser.add_argument(--height, typeint, default180, help小窗高度) parser.add_argument(--offset-x, typeint, default20, help距离目标窗口的横向偏移) parser.add_argument(--offset-y, typeint, default0, help距离目标窗口的纵向偏移) parser.add_argument(--interval, typeint, default300, help轮询间隔毫秒) return parser.parse_args() if __name__ __main__: args parse_args() set_dpi_awareness() finder WindowFinder( title_keywordargs.title, process_nameargs.process, ) app FollowWindow( finder, widthargs.width, heightargs.height, offset_xargs.offset_x, offset_yargs.offset_y, interval_msargs.interval, ) app.run()启动命令很简单python main.py --title Codex如果你希望小窗显示在 Codex 窗口右侧更远一点可以调整横向偏移python main.py --title Codex --offset-x 30如果你希望按进程名匹配可以这样运行python main.py --process codex.exe注意当 Codex 以 CLI 方式跑在 Windows Terminal 里时真正可见的窗口进程是 Windows Terminal不是 codex。因此优先使用--title指定终端标题关键字。5. 运行与验证启动后屏幕上会出现一个深色的小悬浮窗默认显示“跟随中”。如果你已经打开了 Codex 窗口小窗会自动出现在 Codex 窗口右侧约 20 像素的位置。可以按照下面的步骤验证功能。第一步拖动 Codex 窗口观察小窗是否跟着移动。小窗应该保持相对位置不变始终停留在 Codex 窗口的右侧。第二步最小化 Codex 窗口。小窗会自动隐藏而不是停留在原地。第三步恢复 Codex 窗口。小窗会自动出现并再次回到正确位置。第四步点击小窗上的“暂停”按钮小窗会停止跟随并显示“已暂停”。这时候再拖动 Codex 窗口小窗不会移动。再次点击按钮恢复跟随。第五步点击“退出”按钮程序结束小窗消失。如果你运行后小窗没有出现运行下面的脚本查看当前系统所有可见窗口的标题确认目标窗口标题关键字是否正确。# list_windows.py import win32gui def list_windows(): def cb(hwnd, extra): if win32gui.IsWindowVisible(hwnd): title win32gui.GetWindowText(hwnd) if title: print(hwnd, title) return True win32gui.EnumWindows(cb, None) if __name__ __main__: list_windows()这个脚本会把所有带标题的可见窗口列出来。找到你的 Codex 窗口把标题中的关键部分作为--title参数传入即可。6. 常见问题与排查窗口跟随工具本身不复杂但实际使用中经常会遇到几个固定问题。下面整理成表格方便快速排查。问题现象常见原因解决思路小窗一直没有出现目标窗口标题没有匹配到使用 list_windows.py 查看实际窗口标题调整--title小窗出现在错误的位置DPI 缩放未处理启动时调用set_dpi_awareness()并且要在创建窗口前调用小窗闪烁或抖动轮询间隔太短坐标变化检测不灵敏把--interval调到 300 以上同时保持阈值判断小窗跟着终端走而不是跟着 Codex 内容走Codex 跑在终端内部窗口属于终端进程按终端窗口标题匹配或者改用 Codex 桌面端小窗在 Codex 恢复后没有重新出现隐藏状态判断异常检查winfo_viewable()和hidden标志的配合逻辑找了很多次都找不到窗口Codex 窗口被最小化或隐藏先恢复 Codex 窗口再启动小工具最常见的问题是“找不到窗口”。这里需要说明一下窗口枚举只能找到可见的顶层窗口。如果 Codex 窗口被最小化到任务栏或者以托盘图标方式运行顶层窗口可能仍然存在但可见性状态会有变化。因此建议先让 Codex 窗口正常显示再启动跟随工具。另一个需要注意的问题是“目标窗口被关闭后重新打开”。Codex 桌面端如果退出再启动窗口句柄会变化。当前示例代码已经在轮询过程中判断IsWindow无效时重新调用finder.find()所以能处理这种情况。但如果是按标题匹配而新打开的窗口标题和旧窗口不同就需要重新调整参数。7. 工程化建议与扩展方向这个跟随窗口做成小工具后还可以继续完善几个方向。首先是打包成 exe。用 PyInstaller 打包后可以脱离 Python 环境直接运行适合日常开机启动。pip install pyinstaller pyinstaller -F -w main.py-F表示打包成单个可执行文件-w表示不显示命令行黑窗口。打包后的 exe 可以直接放到桌面或任务栏配合 Codex 使用。其次是增加全局快捷键。现在暂停和继续需要通过点击小窗按钮完成但小窗本身比较小点击不太方便。可以引入pynput库注册一个全局快捷键比如Ctrl Alt F用来切换暂停状态。需要注意全局快捷键的回调运行在独立线程中不能直接修改 Tkinter 控件需要通过root.after把操作调度回主线程。再次是支持窗口相对位置选择。目前小窗固定显示在目标窗口右侧可以通过参数增加--position right、--position left、--position top、--position bottom等模式。根据位置参数计算坐标时只需要调整目标窗口四个边界的计算方式。多显示器环境下建议配合SetProcessDpiAwareness(2)使用。这样每个显示器上的坐标都是真实的物理像素坐标辅助窗口在跨屏拖动时不会出现明显的偏移。最后是安全边界。这种窗口跟随工具本质上是读取和设置窗口位置属于常见桌面自动化能力。使用时要保持合理用途比如辅助自己开发、做学习实验。不要把它用于未经授权的窗口控制或干扰其他人使用。代码中也尽量不要把轮询间隔设置得太短这会给系统带来无意义的资源消耗。300 毫秒的轮询间隔已经足够平滑。8. 总结与下一步这个“会跟着 Codex 走的窗口”本质上是一个最小可用的 Windows 窗口跟随工具。通过EnumWindows枚举窗口、GetWindowRect读取坐标、Tkintergeometry设置位置配合定时轮询就实现了辅助窗口自动贴住主窗口的效果。代码本身不依赖复杂框架核心逻辑可以迁移到其他场景。比如让一个工具栏跟随浏览器窗口让一个状态面板跟随游戏窗口或者让一个监控小窗跟随数据库客户端窗口。理解了窗口句柄、窗口坐标、DPI 感知这几个基础概念之后你就能根据实际需求定制自己的桌面辅助工具。下一步你可以尝试两件事一是把代码里的跟随逻辑抽象成独立函数接入pynput全局热键二是用 PyInstaller 打包成 exe集成到日常开发环境中。如果本文对你有帮助可以收藏备用后面遇到窗口布局问题再翻出来看看。