
win32 获取鼠标位置和移动窗体看着只是十几行代码真正扔进 WPF 项目里跑问题会一个接一个冒出来GetCursorPos 返回的是屏幕物理像素窗体的 Left/Top 要的是设备无关单位中间又夹着 MouseCursor_Point 的逐次累加鼠标拖快一点、换个显示器窗口就可能漂出去十几像素甚至甩到屏幕外面。这篇走的是「接入配置 改写」这条线先在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建一把 Key把 Codex 的 Base URL 填成 https://taotoken.net/api再把原文那三段拖窗代码整段丢给它让它按「一次性记录拖拽偏移量」的思路重排。下面按原文的顺序来先拆清楚 GetCursorPos 加 MouseMove 为什么容易偏再配通 Codex 的通道最后看改写后的写法长什么样。1. GetCursorPos 和 MouseMove 拼出来的拖窗为什么会越拖越偏原文的思路非常直白MouseDown 的时候记住当前鼠标在屏幕上的位置MouseMove 的时候再取一次两次相减得到位移累加到 Left/Top 上。这个模型在理想情况下是对的Mono 之前的 WinForm 时代大家也这么写。问题在于它把「鼠标位置」当成了唯一状态却没有把「鼠标相对窗体的落点」固定下来只要中间有任何一次事件被丢掉、被合并误差就永久留在窗口坐标里。1.1 原文 MouseDown 里存下的那一个 MouseCursor_Point原文 MouseDown 的写法是左键按下时把System.Windows.Forms.Cursor.Position存进字段MouseCursor_Point。注意这里用的是 WinForms 的静态属性读到的同样是屏幕物理像素。它没有记录鼠标落在窗体内部的哪一点所以窗口一旦被移动这个「起点」就只对下一次 Move 有效。换句话说整套逻辑是相对位移累加型不是绝对锚定型。累加型的通病就是只要有一次差值算错后面每一次都在错的基础上继续加。1.2 四个 if/else 分支把误差一次次累加原文为了避免负数减法把 X 和 Y 各拆成了两个分支MouseCursor_Point_Aux.X MouseCursor_Point.X就加否则就减。从数学上讲这两种写法是等价的但代码变长了两倍而且每一帧都在读写this.Left和this.Top。WPF 的 Left/Top 是依赖属性每次赋值都会触发布局与渲染相关的通知拖拽过程中高频赋值配合显示器缩放就很容易看到窗口边缘抖一下、或者跟手慢半拍。再加上窗口本身还有最小化、吸附、任务栏避让这类系统行为逐帧累加的结果就是肉眼可见的漂移。1.3 System.Windows.Forms.Cursor.Position 与 e.GetPosition 混用的代价原文紧接着又出现了e.MouseDevice.GetPosition(curtainCanvas)用来取鼠标在窗体内部的坐标。这其实已经是 WPF 的正确姿势了返回的是相对于 canvas 的设备无关坐标。一个文件里同时出现 WinForms 的屏幕像素和 WPF 的逻辑坐标就是后面所有对不齐的根源。更省事的做法是拖拽位移的参照物改成鼠标在窗口内的落点只在需要绝对屏幕坐标时才调用 GetCursorPos。原文注释里那行//this.DragMove();也说明作者当时犹豫过——DragMove()对普通 Window 是好用的但如果拖拽手柄是窗口内的一个 Border就得写成Window.GetWindow(this).DragMove()而且它会进入系统模态循环中途想插自定义逻辑会别扭所以才有了手写 Left/Top 这条路。2. 让 Codex 来改这三段代码之前先把它的 Base URL 填成 TaoToken 的通道代码本身不难难的是把「DPI 换算、多屏坐标、捕获鼠标」这几件事一次性讲清楚。这种活交给 Codex 很合适但前提是它得先能稳定跑起来。官方入口在高峰期排队、额度说没就没的时候改代码的节奏会被打断所以这里先把 Codex 的请求通道指到 TaoToken 上。2.1 在官网创建一把 Key打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 注册登录后进控制台在 API Keys 页面创建一把新的 Key复制出来存好。页面里的模型广场可以顺便看一眼当前可用的模型 ID后面填配置要用。Key 不要在聊天窗口、issue、截图里明文传本地放到环境变量里最省心。这里创建的 Key 只用于给工具做鉴权和写进代码里的业务逻辑没有任何关系改完拖窗代码记得别把它提交进仓库。2.2 ~/.codex/config.toml 里的 model_provider 与 base_urlCodex 的配置走的是~/.codex/config.tomlWindows 下是%USERPROFILE%\.codex\config.toml。要做的就是加一个自定义 provider把base_url指向 https://taotoken.net/api 注意末尾不要带/v1也不要往这个地址上拼任何查询参数model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chatmodel那一行的具体取值以模型广场当时列表为准不要凭印象手写带日期的后缀。env_key写的是环境变量名不是 Key 本身这样配置文件可以随便放Key 只留在你的 shell 环境里。2.3 模型 ID 与环境变量的填法环境变量按你用的终端来设。macOS 或 Linux 的 zsh/bashexport TAOTOKEN_API_KEYYOUR_API_KEYWindows PowerShell$env:TAOTOKEN_API_KEY YOUR_API_KEY设完重开一个终端让 Codex 读到新变量。这套配置和 Claude Code 的环境变量完全不是一回事别把ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN那套搬到 Codex 上Codex 不认。填完先跑一条最简单的提问确认它回话再让它动拖窗代码。3. 给 Codex 的提示词把 GetCursorPos、MouseCursor_Point、Left/Top 重排成偏移量写法配置通了以后真正决定改写质量的是提示词。把原文那三段代码连同你的诉求一起贴给 Codex比只写一句「帮我优化拖窗」有效得多。要让它知道现在的实现是什么样、哪里疼、改成什么样算好。3.1 提示词里必须写死的几条约束可以直接用下面这段当骨架把文件路径和控件名换成你自己的下面是一段 WPF 拖窗代码用 Win32 GetCursorPos 取屏幕坐标 在 MouseMove 里和上一次的位置相减再累加到 this.Left / this.Top。 问题 1. 逐帧累加拖久了会漂 2. GetCursorPos 是物理像素Left/Top 是 DIP高 DPI 下偏移 3. 按下时没有记录鼠标相对窗口的落点。 请改成绝对锚定写法 - MouseDown 时用 e.GetPosition(this) 记录落点并 CaptureMouse - MouseMove 时用 GetCursorPos 取屏幕坐标转成 DIP 后一次性算 Left/Top - MouseUp 时 ReleaseMouseCapture - 保留原有的 POINT 结构与 DllImport 声明不要引入第三方库。提示词里把「不要引入第三方库」「保留原结构」写清楚Codex 就不会顺手给你换成一套 NuGet 包或者换掉 P/Invoke 声明。这种约束在改老项目时特别关键能省掉一轮「它改得挺好但编译不过」的返工。3.2 改写后的拖窗代码按上面的约束得到的核心结构大致是这样。先是 P/Invoke 和结构体保持和原文一致using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Input; internal static class NativeMethods { [DllImport(user32.dll, SetLastError true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetCursorPos(out POINT lpPoint); } [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; public POINT(int x, int y) { X x; Y y; } }拖拽逻辑改成绝对锚定MouseCursor_Point这个逐帧更新的字段就可以退休了取而代之的是「按下时鼠标在窗口里的落点」private Point _dragAnchorInWindow; private bool _dragging; private void g1_MouseDown(object sender, MouseButtonEventArgs e) { if (e.LeftButton ! MouseButtonState.Pressed) return; _dragging true; _dragAnchorInWindow e.GetPosition(this); // DIP相对窗口左上角 ((UIElement)sender).CaptureMouse(); e.Handled true; } private void g1_MouseMove(object sender, MouseEventArgs e) { if (!_dragging || e.LeftButton ! MouseButtonState.Pressed) return; if (!NativeMethods.GetCursorPos(out POINT screen)) return; var dip ScreenPxToDip(new Point(screen.X, screen.Y)); this.Left dip.X - _dragAnchorInWindow.X; this.Top dip.Y - _dragAnchorInWindow.Y; } private void g1_MouseUp(object sender, MouseButtonEventArgs e) { _dragging false; ((UIElement)sender).ReleaseMouseCapture(); }和原文最大的差别有三点差值只算一次而不是每帧累加Left/Top 每帧都是从头算出来的绝对值前一次算错不会污染后一次鼠标捕获交给 WPF 自己的CaptureMouse不再依赖 WinForms 的静态光标状态。3.3 高分屏与多屏ScreenPxToDip 这一层不能省GetCursorPos 给的是物理像素Left/Top 用的是设备无关单位两者之间差一个缩放因子。这个换算拿窗口自己的CompositionTarget来做最稳private Point ScreenPxToDip(Point screenPx) { var source PresentationSource.FromVisual(this); if (source?.CompositionTarget null) return screenPx; // TransformFromDevice设备像素 - DIP return source.CompositionTarget.TransformFromDevice.Transform(screenPx); }注意这个变换是相对于窗口当前所在显示器的。窗口横跨两块缩放比例不同的屏幕时靠近边界的位置会有一点点偏差这是坐标空间本身的性质不是算法的问题。真要做跨屏无缝得先用MonitorFromPoint拿到目标显示器再取对应变换这一步可以留给 Codex 继续迭代——把偏差现象描述清楚让它在这个函数里加分支就行。3.4 窗体内取点e.MouseDevice.GetPosition(curtainCanvas) 的对照原文用e.MouseDevice.GetPosition(curtainCanvas)取鼠标在窗体内部的位置这个写法是推荐的返回值已经是相对于 canvas 的 DIP。它和拖窗用的e.GetPosition(this)只差一个参照物前者相对于某个具体控件后者相对于整个窗口。两个都别和GetCursorPos混着用在同一处计算里混用就回到了 1.3 节说的老问题。另外e.GetPosition在 MouseMove 里调用是有成本的如果只是拖拽一帧调一次足够不用在每个分支里重复取。4. 代码贴回本地编译之后怎么确认这次 Codex 请求走了 TaoToken改写结果只是建议编译、运行、拖一拖都得你自己在本地做。Codex 不会替你连本地环境跑程序它给的是代码和解释验证动作在人这边。这一步顺手把通道也验了如果这次改写请求在控制台里能看到记录说明 Codex 的 Base URL 和 Key 都是对的。4.1 控制台里对一下这次调用回到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的控制台看用量或调用记录找到时间最近的那次 Codex 请求。能看到模型、时间、消耗就算通了。如果记录里根本没有条目说明请求没走到这条通道上先回去检查~/.codex/config.toml里的base_url是不是 https://taotoken.net/api 、末尾有没有被顺手加上/v1。4.2 拖拽自查清单编译通过以后按下面几个动作过一遍基本能覆盖常见的坑慢速拖动窗口看边缘有没有规律性跳动快速甩动后再松手看窗口有没有冲到屏幕外在 125% 或 150% 缩放的显示器上重复上面两步窗口拉大到跨两块屏检查边界处是否有一小段对不齐在 MouseMove 中途松开左键确认没有残留的拖拽状态。最后一条最容易漏。原文没有 MouseUp 处理如果改用绝对锚定却没有清_dragging松手后窗口会继续跟着鼠标跑。加一条 MouseUp 就够别靠e.LeftButton的瞬时状态兜底。5. 排障Codex 侧看 401/404WPF 侧看抖动与偏移问题基本分两堆一堆在配置和通道上一堆在坐标空间上。分开看会快很多。5.1 Codex 的 base_url 与 Key如果 Codex 报鉴权失败先去终端确认TAOTOKEN_API_KEY真的有值再确认config.toml里env_key写的是变量名而不是 Key 本身。如果报找不到接口八成是base_url末尾多了/v1——这个地址本身已经带上版本段了工具会自动拼后面的路径再加一层就变成两层版本号。还有一种情况是改了配置没重启 Codex配置是在启动时读的改完重开一次最省事。5.2 WPF 拖拽的三种典型表现窗口跟手但慢半拍多半是每帧都在给 Left/Top 赋值触发了过多的布局计算绝对锚定写法会缓解这一点。窗口在高分屏上偏移固定的几十像素换算层没做或者用了TransformToDevice的反向变换。跨屏时卡在中间不动坐标变换跟着窗口当前所在的显示器走窗口一部分在另一块屏上时会出现跳变这个要靠 per-monitor 的处理来解决属于进阶项先把单屏拖顺了再折腾。6. 接着往下走把它变成日常改代码的固定动作这一轮改完你手里应该有两样东西一份不靠累加的拖窗代码和一条已经验证过的 Codex 通道。下次再碰到 GetCursorPos 取错、MouseMove 抖动、DPI 对不齐这类问题不用重新折腾配置直接把现象和代码贴给 Codex 就行。想先试试别的模型对这段代码的理解可以去 模型对话 用同一把 Key 发一条消息对照如果每天都靠它改代码Coding Plan 里的套餐值不值得上看一眼自己控制台的用量曲线就有数。新 Key 在 控制台 API Keys 创建配好之后第一件事还是拖一遍窗口——漂不漂手一推就知道。