
如果你经常用 Codex 这类 AI 编程代理工具大概率遇到过这种场景主窗口占据了屏幕的左侧你想看的状态面板却固定停在屏幕右侧你拖动主窗口换个位置后辅助面板仍然悬在原处就像一张贴错了位置的便利贴。窗口越多这种“空间失联”带来的割裂感就越明显。为了彻底解决这个问题我做了一个会跟着 Codex 走的窗口。它不是普通悬浮窗而是通过 Windows 底层窗口事件监听目标窗口的位置变化目标窗口移动到哪里它就自动贴靠到哪里。整个过程不需要额外操作也没有明显的延迟感。这篇文章不打算只展示一个“能跑”的脚本而是从问题拆解、方案选型、核心原理到完整代码、验证方法和常见坑完整讲清楚。即使你完全没接触过 Win32 窗口管理跟着做也能在半小时内跑起来。更重要的是这个方案的思路并不绑定 Codex任何你能枚举到窗口句柄的进程都可以用同样的方式做“跟随窗口”。1. 这篇文章真正要解决的问题1.1 表面的问题是窗口位置本质问题是上下文连续性使用 Codex 这类编程代理工具时实际工作窗口往往不止一个。Codex 主窗口负责展示执行任务、输出日志旁边可能要放一个任务清单、快捷键速查、模型状态面板或者在录屏演示时放一个独立的进度提示窗口。这些辅助窗口如果使用固定坐标一旦主窗口被拖动、最大化、切换显示器就会悬空在旧位置。人要重新把视线从一个窗口跳到另一个窗口这不是“多一个窗口”的问题而是频繁打断心流。所以这里真正的问题不是“做一个悬浮窗”而是让辅助窗口与目标窗口保持稳定的空间关联。窗口跟随技术要解决的核心问题就是把这种关联从人工维护变成自动化维护。1.2 什么样的读者最适合读这篇文章有两类人最应该读一类是 Codex 的重度用户。你希望主工作窗口旁边始终有个贴靠的辅助信息区但又不想手动调整位置。另一类是做桌面自动化和窗口管理工具的人。这篇文章会拆解 Win32 窗口枚举、事件钩子、坐标计算、窗口移动等关键 API这些知识在录屏辅助、演示工具、直播伴侣、多窗格布局工具里都能复用。1.3 先给结论技术选型的判断在 Windows 平台做窗口跟随有两个主流方案轮询方案定时调用GetWindowRect获取目标窗口位置然后调用SetWindowPos移动自己的窗口。实现简单但有延迟且轮询间隔太密会白白消耗 CPU。事件驱动方案调用SetWinEventHook监听窗口移动、切换等事件事件发生时立刻更新。实时性好开销低但需要理解 Windows 消息循环。我的选择是以事件驱动为主轮询作为兜底。这个组合既保证了响应速度又避免了因为个别事件漏报导致窗口失联。下面会详细说明实现思路。2. 窗口跟随的核心概念与原理2.1 窗口句柄与窗口矩形Windows 中的每个可见窗口都有一个唯一标识叫做 HWND窗口句柄。要让一个窗口跟随另一个窗口首先要拿到目标窗口的 HWND。拿到 HWND 后通过GetWindowRect可以获取窗口在屏幕上的坐标矩形也就是窗口的 Left、Top、Right、Bottom 四个值。注意这里的坐标是屏幕坐标以屏幕左上角为原点向右为 X 轴向下为 Y 轴。在多显示器环境下副屏的坐标可能包含负值这一点后面处理 DPI 和坐标时会再次提到。2.2 三种实现方式的对比方案原理优点缺点适合场景定时轮询每隔一段时间读取目标窗口位置再移动跟随窗口实现简单跨语言容易有延迟轮询过密时 CPU 占用高对实时性要求不高的场景WinEvent 事件钩子通过SetWinEventHook监听窗口移动、前台切换等事件实时性好事件触发才处理需要消息循环API 理解成本稍高需要顺滑跟手效果系统级窗口吸附借助第三方窗口管理器实现边角吸附无需开发不够灵活绑定特定窗口管理器不想写代码的场景2.3 关键 API 的作用整套方案依赖四个核心 APIEnumWindows枚举桌面上的所有顶层窗口。GetWindowThreadProcessId根据窗口句柄反查进程 ID用来和进程名对应。GetWindowRect获取目标窗口矩形。SetWindowPos移动并调整目标窗口大小。事件驱动部分还需要SetWinEventHook和UnhookWinEvent。其中SetWinEventHook可以注册一个回调函数当窗口位置变化EVENT_OBJECT_LOCATIONCHANGE或前台窗口切换EVENT_SYSTEM_FOREGROUND时会收到事件通知。2.4 容易被忽略的“窗口树”问题Codex 在不同的运行方式下窗口形态不一样。如果 Codex 跑在 Windows Terminal 里那么你从桌面枚举到的窗口通常是 Windows Terminal 的宿主窗口而不是内部某个标签页的小窗口。如果跑在 VSCode 的终端面板里窗口又是 VSCode 的窗口。所以定位目标窗口时不能只按标题匹配“codex”要考虑当前 Codex 实际运行在什么宿主中。更可靠的做法是按进程名先缩小范围再用窗口标题做二次过滤。这样即使 Codex 终端标题会变化只要能匹配进程名窗口依然可以找得到。Codex 运行形态真正需要跟随的窗口Codex CLI 直接运行在 cmd/PowerShell对应控制台窗口Codex 运行在 Windows TerminalWindows Terminal 宿主窗口Codex 插件运行在 VSCodeVSCode 主窗口Codex 桌面版桌面版主窗口这也是实际操作中新手最容易踩的坑枚举出来的窗口很多直接按标题找不一定拿得到正确句柄。3. 环境准备与前置条件3.1 系统与运行环境本文示例以 Windows 10/11 为主Python 版本建议 3.10 及以上。代码中主要使用 Python 标准库加pywin32示例以通用思路为主版本请以你的实际环境为准。安装依赖pip install pywin32如果你希望少引入一个第三方库也可以用ctypes直接调用user32.dll中的窗口 API。本文会给出两种方式的示例代码。3.2 确认 Codex 环境是否正常在开始写窗口工具之前先确认 Codex 本身可以正常启动codex --version codex login如果登录或模型配置有问题窗口工具做得再好也没有实际意义。这里多说一句如果你给 Codex 配置了第三方模型网关或本地代理遇到类似cc switch local proxy failed while handling codex endpoint /responses的报错通常说明 Codex CLI 在请求/responses端点时连接失败。排查顺序是先确认网关服务是否已启动、地址是否可访问再检查接入配置里的 base_url 和密钥最后看调试日志。本地 API 网关的配置需要遵守平台和服务商的使用规范。3.3 推荐准备一个窗口查看工具开发窗口自动化工具时强烈建议准备一个窗口信息查看工具例如 SpyVisual Studio 自带或者常用窗口管理器中的“窗口详情”功能。这类工具可以实时看到窗口的标题、类名、进程 ID、窗口矩形等关键信息能帮你快速确认目标窗口到底对应哪个 HWND。如果没有也可以先用 PowerShell 查看窗口列表Get-Process | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object Id, ProcessName, MainWindowTitle这一步能让你快速确认Codex 当前所在的宿主进程叫什么名字窗口标题是什么HWND 是否已经能被系统识别。3.4 安全边界提醒窗口自动化属于系统级操作务必遵守几个底线只操作你自己启动的进程窗口或者在合理范围内有权管理的窗口。不要尝试用窗口钩子去截获其他进程的键盘输入等敏感信息。不要在未授权环境中部署这类工具。涉及生产环境或别人机器时先获取明确授权。4. 核心流程拆解窗口跟随整体可以拆成五个步骤定位目标窗口、注册事件监听、计算跟随位置、移动跟随窗口、组合与清理。4.1 定位目标窗口第一步是找到 Codex 所在宿主窗口的 HWND。我推荐按“进程名 窗口标题”的组合方式过滤。流程是这样的调用EnumWindows枚举所有顶层窗口。对每个窗口调用GetWindowThreadProcessId拿到进程 ID。根据进程 ID 获取进程名先过滤掉不相关的进程。再根据窗口标题关键字做二次匹配排除同名进程下的杂散窗口。如果目标窗口暂时不存在比如 Codex 还没启动工具要进入等待状态而不是直接崩溃。4.2 注册事件钩子拿到目标 HWND 后注册事件钩子监听EVENT_OBJECT_LOCATIONCHANGE目标窗口移动、缩放时触发。监听EVENT_SYSTEM_FOREGROUND目标窗口成为前台窗口时触发一次重新贴靠。需要特别注意的是SetWinEventHook的回调函数必须保持引用不能被垃圾回收。如果回调被回收事件会悄悄失效表现就是“窗口偶尔不跟着走”。事件驱动还有一个前提当前线程需要进入消息循环否则事件回调不会被执行。这就是为什么完整示例里往往会包含一个MessageLoop或图形界面主循环。4.3 计算跟随位置当目标窗口位置变化时我们需要计算自己的辅助窗口应该出现在哪里。常见的跟随模式有三种右侧贴靠辅助窗口出现在目标窗口右侧上下对齐。左侧贴靠辅助窗口出现在目标窗口左侧。底部贴靠辅助窗口出现在目标窗口下方。计算逻辑很简单从目标窗口矩形取出 Left、Top、Right、Bottom然后加上偏移量即可。例如右侧贴靠x target_rect.Right offset_x y target_rect.Top offset_y但是这里必须考虑屏幕边界。如果目标窗口本身已经靠近屏幕右侧辅助窗口就会被挤出屏幕。更稳妥的做法是先按默认模式计算位置如果越界则自动切换到屏幕内侧的备用位置。4.4 移动跟随窗口移动窗口使用SetWindowPos或MoveWindow。SetWindowPos更灵活可以同时控制目标坐标 X、Y。窗口宽度、高度。是否置顶。是否显示/隐藏。注意如果你用的是 Tkinter/PySide 这类 GUI 框架最好通过框架提供的强制刷新逻辑更新窗口位置而不是直接在事件回调里操作非线程安全的对象。如果回调运行在独立线程中要先通过队列把位置信息交给主线程处理。4.5 组合逻辑与清理完整流程还需要考虑以下场景目标窗口被关闭或进程退出时辅助窗口自动隐藏而不是悬浮在空白位置。目标窗口再次出现时辅助窗口自动恢复。程序退出时必须调用UnhookWinEvent释放事件钩子避免钩子泄漏。5. 完整示例代码实现下面给出一个可运行的完整示例。代码会分成三部分窗口枚举工具、事件监听器、Tkinter 悬浮窗。你可以直接把所有代码保存为codex_follow_window.py运行。5.1 窗口枚举与矩形获取# 文件路径window_utils.py import win32gui import win32process import win32con def find_window_by_process_name(process_name: str, title_keyword: str ) - int | None: 根据进程名和窗口标题关键字找到顶层窗口句柄。 :param process_name: 进程名例如 WindowsTerminal.exe :param title_keyword: 窗口标题关键字可留空 :return: HWND 或 None result [] def enum_callback(hwnd, _): if not win32gui.IsWindowVisible(hwnd): return True try: _, pid win32process.GetWindowThreadProcessId(hwnd) process win32gui.GetModuleFileNameEx(win32gui.GetWindow(hwnd, win32con.GW_OWNER), 0) except Exception: return True # 兜底用 win32process 获取进程 PID然后用进程名匹配 import psutil try: proc psutil.Process(pid) except psutil.NoSuchProcess: return True if proc.name().lower() ! process_name.lower(): return True title win32gui.GetWindowText(hwnd) if title_keyword and title_keyword.lower() not in title.lower(): return True result.append(hwnd) return False win32gui.EnumWindows(enum_callback, None) return result[0] if result else None def get_window_rect(hwnd: int) - dict: 获取窗口矩形返回屏幕坐标。 left, top, right, bottom win32gui.GetWindowRect(hwnd) return { left: left, top: top, right: right, bottom: bottom, width: right - left, height: bottom - top, }上面代码里用到了psutil先安装pip install psutil这段代码的作用是遍历桌面顶层窗口按进程名匹配出 Codex 所在的宿主窗口并返回它的句柄。如果你不想引入psutil可以用win32process.GetModuleFileNameEx直接拿进程路径再取文件名效果类似。5.2 SetWinEventHook 事件监听器接下来是事件监听部分。这里我用ctypes调用user32.dll避免把逻辑全部压在pywin32上。# 文件路径window_events.py import ctypes import threading import queue import time user32 ctypes.windll.user32 EVENT_OBJECT_LOCATIONCHANGE 0x800B EVENT_SYSTEM_FOREGROUND 0x0003 WINEVENT_OUTOFCONTEXT 0x0000 # 回调类型CALLBACK Proc WinEventProcType ctypes.WINFUNCTYPE( None, ctypes.c_void_p, # hWinEventHook ctypes.c_uint, # event ctypes.c_void_p, # hwnd ctypes.c_long, # idObject ctypes.c_long, # idChild ctypes.c_uint, # dwEventThread ctypes.c_uint, # dwmsEventTime ) class WindowEventWatcher: def __init__(self, target_hwnd, callback): self.target_hwnd target_hwnd self.callback callback self._hook None self._queue queue.Queue() self._thread None def _event_proc(self, h_event_hook, event, hwnd, id_object, id_child, event_thread, event_time): if hwnd self.target_hwnd: if event EVENT_OBJECT_LOCATIONCHANGE or event EVENT_SYSTEM_FOREGROUND: # 放到队列里避免在 API 回调中直接做重量级操作 self._queue.put((event, hwnd)) def start(self): self._thread threading.Thread(targetself._message_loop, daemonTrue) self._thread.start() def _message_loop(self): callback WinEventProcType(self._event_proc) # 保存引用防止被 GC 回收 self._callback_ref callback self._hook user32.SetWinEventHook( EVENT_OBJECT_LOCATIONCHANGE, EVENT_SYSTEM_FOREGROUND, 0, callback, 0, 0, WINEVENT_OUTOFCONTEXT, ) if not self._hook: print(SetWinEventHook failed) return # 进入消息循环等待事件回调执行 msg ctypes.wintypes.MSG() while user32.GetMessageW(ctypes.byref(msg), None, 0, 0) ! 0: if msg.message 0x0012: # WM_QUIT break user32.TranslateMessage(ctypes.byref(msg)) user32.DispatchMessageW(ctypes.byref(msg)) def process_events(self): 由主线程调用处理队列中的事件。 while not self._queue.empty(): event, hwnd self._queue.get() self.callback(event, hwnd) def stop(self): if self._hook: user32.UnhookWinEvent(self._hook) self._hook None这段代码的关键点有三个回调函数是ctypes.WINFUNCTYPE包装的必须保存引用否则会被回收。回调本身只把事件放入队列不直接做窗口移动避免在系统回调线程里操作 GUI 对象。监听线程进入GetMessageW消息循环事件才能持续触发。5.3 Tkinter 悬浮窗与跟随主逻辑下面把前面两部分串起来创建一个小窗口跟随 Codex 宿主窗口。# 文件路径codex_follow_window.py import json import time import tkinter as tk from window_utils import find_window_by_process_name, get_window_rect from window_events import WindowEventWatcher class FollowWindow: def __init__(self, config): self.config config self.target_hwnd None self.root tk.Tk() self.root.title(Codex Follow) self.root.geometry(f{config[window][width]}x{config[window][height]}) self.root.resizable(False, False) self.root.attributes(-topmost, config[window][topmost]) if config[window].get(opacity): self.root.attributes(-alpha, config[window][opacity]) # 放一个简单的辅助信息标签 label tk.Label( self.root, textCodex 辅助面板\n跟随模式右侧贴靠, font(Microsoft YaHei, 12), padx10, pady20, ) label.pack(fillboth, expandTrue) self._init_target() self._follow_once() # 事件监听 self.watcher WindowEventWatcher(self.target_hwnd, self._on_window_event) self.watcher.start() # 每 500ms 处理一次事件队列 self._poll_events() def _init_target(self): process_name self.config[target][process_name] title_keyword self.config[target].get(window_title_contains, ) self.target_hwnd find_window_by_process_name(process_name, title_keyword) if self.target_hwnd: print(f找到目标窗口: {self.target_hwnd}) else: print(未找到目标窗口进入等待状态...) def _on_window_event(self, event, hwnd): # 事件触发后在下一轮主循环里重新贴靠 self.root.after(0, self._follow_once) def _follow_once(self): if not self.target_hwnd: self._init_target() if not self.target_hwnd: return rect get_window_rect(self.target_hwnd) mode self.config[position][mode] offset_x self.config[position][offset_x] offset_y self.config[position][offset_y] if mode right: x rect[right] offset_x y rect[top] offset_y elif mode left: x rect[left] - self.config[window][width] - offset_x y rect[top] offset_y else: # bottom x rect[left] offset_y y rect[bottom] offset_x # 简单边界保护避免被移出屏幕 screen_width self.root.winfo_screenwidth() screen_height self.root.winfo_screenheight() max_x max(0, screen_width - self.config[window][width]) max_y max(0, screen_height - self.config[window][height]) x max(0, min(x, max_x)) y max(0, min(y, max_y)) self.root.geometry(f{x}{y}) self._ensure_visible() def _ensure_visible(self): if not self.root.state() normal: self.root.deiconify() self.root.lift() self.root.attributes(-topmost, True) def _poll_events(self): if self.watcher: self.watcher.process_events() # 兜底如果事件漏报每 1 秒再跟随一次 self._follow_once() self.root.after(1000, self._poll_events) def run(self): self.root.mainloop() if __name__ __main__: with open(config.json, r, encodingutf-8) as f: cfg json.load(f) app FollowWindow(cfg) try: app.run() except KeyboardInterrupt: if app.watcher: app.watcher.stop()5.4 配置文件{ target: { process_name: WindowsTerminal.exe, window_title_contains: }, position: { mode: right, offset_x: 8, offset_y: 0 }, window: { width: 360, height: 600, topmost: true, opacity: 0.92 } }process_name需要根据实际环境调整。如果你是直接打开一个 cmd 窗口运行 Codex就把这个值改成cmd.exe如果用的是 Windows Terminal则改成WindowsTerminal.exe如果是在 VSCode 里使用则改成Code.exe。5.5 一个轻量的 PowerShell 版本如果你不想用 Python只想验证窗口跟随的核心 API可以用 PowerShell 写一个简单版本。它只做“读取目标窗口矩形 移动跟随窗口”两件事适合理解 API 调用关系。Add-Type using System; using System.Runtime.InteropServices; public struct RECT { public int Left; public int Top; public int Right; public int Bottom; } public class NativeWin32 { [DllImport(user32.dll)] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport(user32.dll)] public static extern bool SetWindowPos( IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport(user32.dll)] public static extern bool MoveWindow( IntPtr hWnd, int X, int Y, int w, int h, bool repaint); } # 替换成实际的窗口句柄 $targetHandle [IntPtr]0x00000000000A0A0A $rect New-Object RECT [NativeWin32]::GetWindowRect($targetHandle, [ref]$rect) | Out-Null # 把当前 PowerShell 窗口移动到目标窗口右侧 $currentPid $PID $currentProcess Get-Process -Id $currentPid [NativeWin32]::MoveWindow( $currentProcess.MainWindowHandle, $rect.Right 8, $rect.Top, 400, 600, $true ) | Out-NullPowerShell 版本只适合快速验证生产级的跟随工具还是建议使用 Python 或 C# 等语言完整处理消息循环、线程和异常。6. 运行结果与效果验证6.1 启动方式先启动 Codex再运行跟随窗口程序python codex_follow_window.py如果程序成功找到目标窗口控制台会输出找到目标窗口: 123456如果输出“未找到目标窗口进入等待状态...”说明进程名或标题关键字不匹配打开任务管理器确认进程名后修改配置文件。6.2 如何判断是否成功判断成功的标准很简单跟随窗口出现在目标窗口右侧上下基本对齐。用鼠标拖动 Codex 宿主窗口跟随窗口在 200ms 内自动贴靠到新位置。最大化或恢复窗口时跟随窗口也会立刻调整。关闭 Codex 后跟随窗口仍然存在但不会做出错误移动重新打开 Codex 后继续跟随。如果拖动窗口时跟随窗口没有反应第一步先看控制台是否还在输出任何错误第二步确认事件钩子是否注册成功第三步检查目标窗口句柄在窗口被关闭后是否失效。6.3 日志验证思路当前版本的示例没有加日志。实际使用中可以在_follow_once里加一行打印print(ftarget rect: {rect}, follow to: ({x}, {y}))这样每次跟随都会输出当前窗口位置和最新坐标。运行几十秒后就能确认事件是否触发、坐标计算是否正确、是否有越界被边界保护修正。7. 常见问题与排查思路问题现象可能原因排查方式解决方案找不到目标窗口输出“未找到”进程名不对或窗口没有设置标题用 PowerShell 查看进程 MainWindowTitle修改 config.json 中的 process_name事件回调不触发跟随窗口不动回调函数被垃圾回收或没有进入消息循环检查 C/Python 中回调引用是否被保存保存回调引用确认监听线程进入 GetMessageW跟随窗口一直在跟随但有明显抖动目标窗口移动时跟随窗口又从目标窗口上读取到新位置形成循环查看日志中坐标是否反复跳动给偏移量设置稳定值或用事件驱动代替密集轮询多显示器下位置不对第二个显示器使用负坐标边界保护逻辑没有考虑打印目标窗口 rect 和 screen 尺寸边界保护使用虚拟屏幕坐标而不是主屏坐标DPI 缩放后窗口位置偏移程序不是 DPI 感知的Windows 对坐标做了缩放检查进程的 DPI 感知设置在 manifest 或代码中声明 PerMonitorV2 DPI 感知Codex 接入第三方模型网关后报 “/responses failed”本地网关服务未启动或配置的 base_url 有问题先访问网关健康检查地址再看 Codex 调试日志修正本地网关配置确保模型版本与端点兼容程序退出后钩子还在占用资源没有调用 UnhookWinEvent查看任务管理器中进程是否残留在 finally 中调用 stop() 释放钩子使用了 CtrlC 退出 mainloop 后窗口未清理Tkinter 主循环被强制中断增加 try/finally用协议 WM_DELETE_WINDOW 处理正常退出逻辑其中“跟踪窗口抖动”是最容易被忽略的问题。如果你在事件回调里直接调用移动窗口再触发目标窗口位置更新就可能出现互相影响。解决方法就是把移动操作放到界面主线程的 after 回调里并且只跟随目标窗口不要让跟随窗口反过来影响目标窗口事件。8. 最佳实践与工程建议8.1 窗口识别规则要精确不要只用窗口标题判断更不要只用进程名。标题可能因为 Codex 任务执行而变化进程名可能匹配到多个窗口。建议在配置中准备三个字段process_name、window_title_contains、class_name用组合条件过滤。8.2 事件驱动为主轮询兜底事件钩子不一定能完全覆盖所有窗口变化场景比如某些第三方框架的窗口移动方式可能不会触发标准位置事件。我建议位置变化事件触发时立刻跟随。同时保留 1 秒一次的低频轮询用于兜底。轮询间隔不要小于 200ms否则 CPU 占用会明显上升。8.3 多显示器与 DPI 问题多显示器和 DPI 缩放是桌面自动化里最麻烦的两个问题。对于多显示器要使用虚拟屏幕坐标范围而不是winfo_screenwidth()这种主屏尺寸。对于 DPI 缩放最好在程序启动时声明 PerMonitorV2 DPI 感知。在 Python 里可以通过ctypes.windll.user32.SetProcessDPIAware()或者调用SetThreadDpiAwarenessContext在进程启动阶段设置。8.4 异常恢复与清理跟随窗口的退出逻辑必须严谨目标进程关闭后要自动隐藏跟随窗口避免窗口悬停在屏幕上。目标进程重新启动后要重新获取句柄并恢复跟随。退出时调用UnhookWinEvent并用日志记录钩子是否释放成功。8.5 发布为独立工具如果团队里多个人都需要这个工具可以用 PyInstaller 打包成 exepip install pyinstaller pyinstaller -F -w codex_follow_window.py打包时可以把config.json作为外部配置文件一起分发不要让同事改代码。工具的权限边界是它只负责移动自己的辅助窗口不应该尝试修改其他无关进程的窗口状态。8.6 关于 Codex 工具链的组合思路窗口跟随只是一个 UI 层能力。如果你需要更进一步可以让跟随窗口的内容和 Codex 状态联动比如读取 Codex 的任务日志、展示当前执行步骤、显示最近一次命令输出。这里的实现思路是通用的跟随窗口的“位置”由窗口事件驱动“内容”由你的业务逻辑驱动。9. 总结与后续学习方向这篇博客把一个看起来很小的窗口跟随需求拆解成了五个明确步骤窗口定位、事件监听、坐标计算、窗口移动、异常清理。所有技术点都围绕 Windows 的 Win32 窗口体系不依赖具体 AI 模型所以这套代码天然适用于 Codex CLI、Windows Terminal、VSCode 等多种形态。对于刚开始接触 Windows 窗口编程的开发者建议按下面的顺序继续深入先熟悉EnumWindows、GetWindowRect、SetWindowPos这几个基础 API。再理解消息循环和事件钩子的线程模型。然后尝试把跟随窗口做成一个可配置的小工具加入多显示器边界、DPI 感知、进程恢复等边界能力。如果想做得更专业可以用 C# WinUI 3 或 WPF 重写效率和样式表现都会更好。你在实际落地时建议先从一个最小可运行版本开始再逐步添加配置项和异常处理。Codex 窗口本身会随着终端形态变化只要你把“定位目标窗口”这一步设计得足够灵活这个跟随窗口工具就能长期可用。希望这篇文章能帮你少踩一些窗口 API 的坑也让你以后看到“窗口自动化”这几个字的时候不再觉得它有多神秘。