记一次开发无畏契约自动切换分辨率实战
无畏契约自动切换分辨率工具
一个 Windows 后台小工具:检测到无畏契约启动时,自动把桌面分辨率切换到你在 NVIDIA 控制面板里自定义的那个分辨率(含刷新率),游戏退出后自动恢复原分辨率。零依赖、极低占用、开机无感运行。
一、背景与需求
本人玩无畏契约(Valorant)时,和我一样的很多玩家会自定义一个4:3低分辨率(拉伸全屏让人物模型变大,便于瞄准),但是我显示器是2K的,平时桌面又想用高分辨率。每次手动在 NVIDIA 控制面板切换很繁琐。
需求拆解:
- 检测无畏契约启动 / 退出
- 自动切换到 NVIDIA 控制面板里已自定义的分辨率(含刷新率、色深)
- 游戏退出后自动恢复切换前的分辨率
- 极低占用,不能影响游戏性能
- 开机自启,无感运行
二、项目流程
- 需求确认:明确"自定义分辨率"指的是已在 N 卡控制面板添加好的那个(不是新建),工具只需"选中并应用"它。
- 环境检查:
- 确认 NVIDIA 显卡与驱动
- 检测当前桌面分辨率
- 从注册表
CustomDisplay解析出用户自定义的分辨率(宽、高、刷新率) - 用
EnumDisplaySettings枚举系统模式,确认该自定义分辨率可用、可切换
- 方案设计:C# 编译成原生 exe(用 Windows 自带 csc.exe)+ WMI 事件监听 +
ChangeDisplaySettingsEx切换 + 托盘图标 + 计划任务开机自启。 - 实现:编写源码、编译脚本、配置文件、安装/卸载脚本。
- 测试:开游戏自动切换正常;发现关游戏不自动恢复的 bug。
- 修复:WMI 的进程退出事件不可靠,加轮询兜底,
lock + 状态标志去重。 - 优化:智能轮询——游戏时每 5 秒检测退出,平时每 30 秒兜底,平时 CPU≈0。
三、实现原理
1. 检测自定义分辨率
NVIDIA 控制面板的自定义分辨率存储在注册表:
HKLM\SYSTEM\CurrentControlSet\Control\Video\<显卡GUID>\<序号>\CustomDisplay
<显卡GUID>每台机器不同,可用 PowerShell 枚举找到 NVIDIA 适配器:
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Control\Video' | ForEach-Object {
$p = Get-ItemProperty (Join-Path $_.PSPath '0000') -ErrorAction SilentlyContinue
if ($p.DriverDesc -match 'NVIDIA') { $_.PSChildName }
}
CustomDisplay是二进制值,里面含自定义分辨率的宽、高、刷新率、时序等。部分分辨率有 ASCII 标签(如CUST:2560x1440x275.000Hz),部分是结构化存储(无标签,需按字节解析)。
更可靠的方式:直接用 EnumDisplaySettings 枚举系统支持的所有显示模式,自定义分辨率会出现在列表里(含刷新率、色深),比逆向二进制更稳。
2. 切换分辨率
核心是两个 Win32 API:
// 枚举所有显示模式,找到目标分辨率的那条 DEVMODE
// 关键:用系统已有的 DEVMODE(含 NVIDIA 自定义时序),不自己造参数
DEVMODE dm = new DEVMODE();
dm.dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODE));
int i = 0;
while (EnumDisplaySettings(null, i, ref dm)) {
if ((int)dm.dmPelsWidth == targetW &&
(int)dm.dmPelsHeight == targetH &&
(int)dm.dmDisplayFrequency == targetHz &&
(int)dm.dmBitsPerPel == targetBpp) {
// 找到了,直接应用
ChangeDisplaySettingsEx(null, ref dm, IntPtr.Zero,
CDS_UPDATEREGISTRY, IntPtr.Zero);
break;
}
i++;
}
为什么不自己构造 DEVMODE:自定义分辨率的时序参数(CVT 减少消隐等)由 NVIDIA 驱动根据注册表生成,自己造参数容易黑屏或不被接受。用 EnumDisplaySettings 拿到的 DEVMODE 是驱动算好的完整时序,直接应用即可,等价于手动在控制面板里选。
3. 检测游戏进程(双保险)
WMI 事件(即时响应):
// 进程启动事件
var startWatcher = new ManagementEventWatcher(
"SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName = 'VALORANT-Win64-Shipping.exe'");
startWatcher.EventArrived += (s, e) => OnGameStart();
startWatcher.Start();
// 进程退出事件
var stopWatcher = new ManagementEventWatcher(
"SELECT * FROM Win32_ProcessStopTrace WHERE ProcessName = 'VALORANT-Win64-Shipping.exe'");
stopWatcher.EventArrived += (s, e) => OnGameStop();
stopWatcher.Start();
轮询兜底(防 WMI 漏事件):
bool wasRunning = false;
while (true) {
bool running = Process.GetProcessesByName("VALORANT-Win64-Shipping").Length > 0;
if (running && !wasRunning) OnGameStart(); // 切换到游戏分辨率
else if (!running && wasRunning) OnGameStop(); // 恢复桌面
wasRunning = running;
// 智能间隔:游戏时 5 秒检测退出,平时 30 秒兜底
Thread.Sleep(wasRunning ? 5000 : 30000);
}
为什么要双保险:实测 WMI 的 ProcessStartTrace(开游戏)可靠,但 ProcessStopTrace(关游戏)偶尔不触发,导致游戏退出后分辨率不恢复。加轮询兜底后,即使 WMI 漏了,轮询也会在 5 秒内发现游戏退出并恢复。lock + inGame 状态标志 做去重,WMI 和轮询同时触发也不会重复操作。
4. 恢复逻辑
切换前,用 EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref dm) 记录当前桌面分辨率(含刷新率、色深),游戏退出后用记录的值调 ChangeDisplaySettingsEx 恢复。这样不管平时用什么分辨率都能正确恢复,无需配置"恢复目标"。
5. 触发进程的选择
无畏契约的进程链:启动器壳 -> VALORANT.exe -> VALORANT-Win64-Shipping.exe(游戏渲染主进程,占内存最大)。
触发点选 VALORANT-Win64-Shipping.exe:它启动 = 真正进游戏,它退出 = 游戏关了。如果选启动器壳,登录后挂机看商店也会触发,不准。
四、文件结构(路径)
项目目录/
├── ValorantResSwitcher.cs # C# 源码(监听 + 切换 + 托盘 + 配置 + 日志)
├── ValorantResSwitcher.exe # 编译产物(无窗口,后台运行)
├── build.bat # 编译脚本(调 Windows 自带 csc.exe)
├── config.ini # 配置:目标分辨率/刷新率/进程名/轮询间隔
├── install.bat # 安装开机自启(计划任务,需管理员)
├── uninstall.bat # 卸载
└── switcher.log # 运行日志(排查用)
关键注册表路径:
HKLM\SYSTEM\CurrentControlSet\Control\Video\<显卡GUID>\0000\CustomDisplay
五、使用方法
1. 配置 config.ini
# 触发的游戏进程名(VALORANT 游戏本体主进程)
process=VALORANT-Win64-Shipping.exe
# 目标分辨率(替换成你在 N 卡控制面板里自定义的那个)
width=1290
height=886
refresh=275
bpp=32
# 轮询间隔(毫秒)。游戏运行时按此间隔检测退出;非游戏时固定 30 秒兜底
poll=5000
2. 编译
双击 build.bat。用 Windows 自带的 .NET Framework C# 编译器,无需安装任何环境。
3. 运行
双击 ValorantResSwitcher.exe,后台运行,托盘出现图标。右键托盘可"立即切换"/"立即恢复"/"退出"。
4. 开机自启
右键 install.bat -> 以管理员身份运行。会创建一个计划任务:登录时以最高权限后台启动,不弹 UAC。
5. 卸载
运行 uninstall.bat。
六、踩坑记录
csc.exe 不支持 C# 6+ 语法:Windows 自带的 .NET Framework
csc.exe(C:\Windows\Microsoft.NET\Framework64\v4.0.30319\)是 C# 5.0 编译器,不支持字符串插值$"..."和 null 条件?.。需改用字符串拼接 + 显式 null 检查。WMI 进程退出事件不可靠:
Win32_ProcessStopTrace偶尔不触发,导致游戏退出后不恢复。加轮询兜底解决。PowerShell 脚本编码问题:中文系统 PowerShell 按 GBK 读取 UTF-8 脚本会乱码、甚至语法错误。脚本输出用英文,或
Add-Type用 here-string,或保存为带 BOM 的 UTF-8。EnumDisplaySettings 的 null 参数:PowerShell 里
$null传给string参数会变成空字符串"",导致枚举返回 0 条。需用[NullString]::Value传真正的 null。DEVMODE 结构体大小:
dmSize必须设为Marshal.SizeOf(typeof(DEVMODE)),结构体字段要完整定义到dmPanningHeight(156 字节),否则EnumDisplaySettings可能返回 false。exe 被占用导致编译失败:重新编译前需先结束正在运行的 exe(任务管理器或
taskkill /im ValorantResSwitcher.exe /f),否则csc写不进 exe 文件。
七、资源占用
| 指标 | 数值 |
|---|---|
| 内存 | 约 34 MB(.NET Framework 运行时 + 托盘图标) |
| CPU(平时) | ≈ 0(WMI 事件驱动 + 30 秒轮询兜底) |
| CPU(游戏时) | 每 5 秒 1-3 ms(GetProcessesByName) |
| 对游戏帧率影响 | 无 |
34 MB 内存是 .NET Framework 的固定开销,想更低需换 native 实现,没必要。CPU 才是影响游戏的关键,事件驱动 + 智能轮询已压到几乎为零。
八、参考资料
- ChangeDisplaySettingsEx function (winuser.h) — Microsoft Learn
- EnumDisplaySettings function — Microsoft Learn
- DEVMODE structure — Microsoft Learn
- Win32_ProcessStartTrace class — Microsoft Learn
- Win32_ProcessStopTrace class — Microsoft Learn
- ManagementEventWatcher Class — .NET API Browser
- C# Compiler Command-Line Options (csc.exe) — Microsoft Learn
- schtasks command — Microsoft Learn
九、扩展思路
- 支持多游戏:config 里配置多个进程名,任一启动即切换。
- 支持多显示器:切换指定显示器而非主屏。
- 支持切换 NVIDIA 缩放模式(全屏拉伸 / 纵横比 / 无缩放),需调用 NVAPI 或改注册表。
- 换 .NET 8 + Native AOT 编译,内存可降到 10 MB 以内。