资讯动态

Windows注册表EDID解析:获取显示器真实型号与序列号

发布时间:2026/10/26 14:50:13 来源:尧图企业网站定制
简介面向使用VC进行Windows系统编程的开发者这份项目源码聚焦从注册表读取显示器EDID二进制数据并解析设备信息这一实用场景适合希望理解硬件信息获取、注册表访问与二进制结构解析的中级学习者参考。压缩包共21个文件约2.38MB包含5个cpp与6个h源文件辅以rc资源脚本、vcproj与sln工程配置、ico图标及ReadMe说明等便于在Visual Studio中查看工程组织与编译结构。目前已有1713人学习。源码围绕EDID的128字节结构展开示范通过RegOpenKeyEx、RegQueryValueEx等API定位Video子键、读取EDID值并将制造商、型号、分辨率、刷新率等字段转换为可读信息同时给出注册表封装类与对话框界面实现可帮助读者掌握二进制数据偏移解析、显示能力判断及系统监测、硬件诊断类应用的开发思路。1. 做显示器盘点被“通用即插即用监视器”卡住的那次做显示器资产盘点、批量验机或售后核对硬件的人大多被同一件事卡过设备管理器里能看到好几台显示器型号却一律是“通用即插即用监视器”序列号一栏空着。想用 VC 调 Windows 接口拿真实参数返回的往往只有一句宽泛的设备描述。真东西其实在注册表里——HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY下每个显示器实例的Device Parameters键里都存着一块名为EDID的二进制值128 字节厂商 PnP ID、产品代码、序列号、物理尺寸、刷新范围全写死在里面。这个项目源码做的就是枚举显示器设备实例、把这块 EDID 抠出来再按 EDID 协议逐字段解析成可读的显示器信息附带一个能直接移植的解析类。适合做设备管理、装机验机工具、资产采集脚本的开发者拿去改。2. 数据链路拆解从设备枚举到注册表 EDID 二进制值在动手写解析之前得先搞清楚这块 128 字节的数据在系统里是怎么放的、能通过哪几条路拿到。我见过不少人一上来就CreateFile打开\\.\DISPLAY1去问显示器型号结果拿到的是适配器名字白折腾半天。这个项目的思路是反过来的先枚举显示器设备实例再顺着实例路径去注册表里取原始 EDID最后自己解协议。这条路最稳因为 EDID 是显示器固件里的原始数据操作系统只是做了个搬运工。2.1 注册表里 EDID 到底藏在哪个键Windows 把每个即插即用设备都挂在HKLM\SYSTEM\CurrentControlSet\Enum下面显示器属于DISPLAY类。层级大概长这样HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY └── ABC1234 ← 硬件ID前3位是厂商PnP ID └── 52f7c1a30UID4352 ← 设备实例ID每台机器不同 └── Device Parameters ├── EDID ← REG_BINARY128或256字节 └── ...键名ABC1234里的ABC就是 EDID 中的厂商 PnP ID后面几位是产品代码。这个值不是猜的是系统从 EDID 里反推出来拼的所以从这里就能先做一次交叉验证。EDID值的类型是REG_BINARY长度一般是 128 字节支持扩展的显示器会是 256 字节前 128 是基础块后 128 是扩展块。真正要读的就是Device Parameters下的这个值。注意路径里有两个容易翻车的点一是Enum\DISPLAY\...中间这段实例 ID 本身就带反斜杠拼路径时别再重复加Enum\二是不同 Windows 版本下CurrentControlSet可能是个符号链接用RegOpenKeyEx打开时让它自己解析就行不要手动拼ControlSet001。2.2 三条获取路径的取舍SetupAPI、EnumDisplayDevices、WMI同一个 EDIDWindows 给了至少三条路项目里我一般优先用 SetupAPI原因是对比一下就清楚了。方式拿到的数据优点局限SetupAPIGUID_DEVCLASS_MONITOR设备实例 ID、硬件 ID、可定位注册表键稳定、覆盖所有已安装显示器、能拿到原始 EDID代码略多要链接setupapi.libEnumDisplayDevicesDISPLAY_DEVICE.DeviceID调用简单一行一个显示器只覆盖当前显示适配器下激活的屏幕虚拟屏、已断开的历史设备拿不到WMIWmiMonitorID类已解析好的厂商、名称、序列号不用自己解 128 字节依赖 WMI 服务精简系统或服务被禁用时直接失败项目选 SetupAPI 做主线是因为要的是“原始 EDID”而不是“已经解析好的字段”。WMI 给的是加工过的字符串遇到需要判断色深、刷新范围、扩展块这类细节时就不够用了而 SetupAPI 拿到实例 ID 之后注册表键就在手边原始数据完整。EnumDisplayDevices我一般只用来做辅助校验比如确认这台显示器当前是否处于活动状态。2.3 工程目录与模块职责划分这套源码的目录结构不复杂核心就是把“读”和“解析”拆开方便单独测试MonitorEdid/ ├── MonitorEdid.sln ├── src/ │ ├── main.cpp // 控制台入口遍历并打印 │ ├── EdidReader.h/.cpp // SetupAPI枚举 注册表读取 │ ├── EdidParser.h/.cpp // 128字节协议解析 │ └── MonitorInfo.h // 输出结构体定义 └── res/EdidReader只负责“拿到字节流”EdidParser只负责“把字节流翻译成结构体”两者之间用一个std::vectorunsigned char传递。这个拆分看着简单但后面加扩展块解析、加缓存、换数据源比如从文件读 EDID dump时改动面会小很多。如果原始工程是 MFC 对话框把EdidParser挂到按钮响应里、把结果填进CListCtrl就行解析层完全不用动。3. EDID 协议拆解128 字节怎么读成显示器参数EDID 是 VESA 定的标准1.3 和 1.4 版本在基础块上基本兼容。项目里解析的是基础块 128 字节字段偏移是固定的不需要动态判断长度。这一章按字节顺序把关键字段过一遍每个字段给出偏移和解析方式后面写代码时直接对着抄。3.1 头部校验与厂商 PnP ID 的 5 位编码前 8 个字节是固定头00 FF FF FF FF FF FF 00不匹配基本可以判定数据无效或读错了键。第 8、9 字节是厂商 ID大端存储的 16 位值但编码方式很特殊三个字母各占 5 位从高位往低位排。// edid[8]、edid[9] 是大端存储的16位厂商码 unsigned short mfg (static_castunsigned short(edid[8]) 8) | edid[9]; char vendor[4]; vendor[0] static_castchar(((mfg 10) 0x1F) A - 1); // 高5位 vendor[1] static_castchar(((mfg 5) 0x1F) A - 1); // 中5位 vendor[2] static_castchar(( mfg 0x1F) A - 1); // 低5位 vendor[3] \0;参数说明每个字母的编码值是“字母序号”A 对应 1不是 0所以解析时要先减 1 再加A。举例0x0443拆出来00001 00010 00011就是ABC。这个值应该和注册表实例 ID 的前三位一致不一致说明数据被动过或者读到了别的设备。产品代码在偏移 10、11小端productCode edid[10] | (edid[11] 8)。序列号在偏移 12 到 15小端 32 位。这两个字段经常被忽略但做资产核对时序列号是唯一标识比显示器名称靠谱得多。3.2 物理尺寸、gamma 与特性位从偏移 16 开始是制造信息和显示参数。制造周在 16制造年在 17年份要加 1990 才是真实年份。偏移 18、19 是 EDID 版本和修订号1.3 和 1.4 在扩展块处理上有差异解析扩展块前先看这两个字节。偏移 21、22 是水平和垂直物理尺寸单位厘米。这两个值用途很大可以反推屏幕对角线英寸数做“屏幕多大”的快速判断。计算公式是sqrt(w*w h*h) / 2.54注意结果会有 0.1 到 0.3 英寸的误差因为厂商上报的是取整后的厘米。double widthCm edid[21]; double heightCm edid[22]; double inch std::sqrt(widthCm * widthCm heightCm * heightCm) / 2.54;偏移 23 是 gamma 值实际 gamma 等于(edid[23] 100) / 100.0。如果这个字节是0xFF表示 gamma 定义在扩展块里基础块里不提供这时候不要硬算标成“未知”更诚实。偏移 20 是视频输入定义第 7 位区分数字还是模拟输入数字输入时低 4 位表示接口类型1 是 DVI、2 是 HDMI-a、5 是 DisplayPort。偏移 24 是特性支持位第 4 位表示显示类型RGB 彩色还是单色第 1 位表示是否支持首选时序。这些位做设备分类很有用比如按接口类型统计一批机器的接线情况。3.3 描述符块显示器名称、序列号与范围限制偏移 54 到 125 是 4 个 18 字节的描述符块这是 EDID 里信息密度最高的部分。每个描述符前 5 个字节是类型标识常见的四种前5字节类型内容00 00 00 FC 00显示器名称偏移 5 起 13 字节 ASCII 文本00 00 00 FF 00序列号字符串同上00 00 00 FE 00无特定字符串同上00 00 00 FD 00范围限制垂直/水平频率、最大像素时钟名称描述符是很多人最想要的字段。13 字节文本以0x0A换行符结尾后面补0x20空格填充。解析时遇0x0A就停再把尾部空格 trim 掉否则拿到的字符串会带一串看不见的空格做匹配时对不上。for (int i 0; i 4; i) { const unsigned char* d edid 54 i * 18; if (d[0] 0 d[1] 0 d[2] 0 d[3] 0xFC) { std::string name; for (int k 5; k 18; k) { if (d[k] 0x0A) break; // 换行符截止 if (d[k] 0x20 d[k] 0x7E) // 只收可见ASCII name.push_back(static_castchar(d[k])); } while (!name.empty() name.back() ) name.pop_back(); // trim尾部空格 info.name name; } }范围限制描述符0xFD的字节 5 到 9 分别是最小垂直频率、最大垂直频率、最小水平频率、最大水平频率、最大像素时钟除以 10单位 MHz。做兼容性检查时把当前分辨率对应的刷新率丢进去比一下超出范围就说明这块屏在这个模式下是超频跑的。4. 核心代码落地读取、校验、解析三段式协议讲完落到代码上就是三个步骤枚举显示器拿到实例 ID、按实例 ID 读注册表里的 EDID 二进制、把字节流解析成结构体。项目里这三步分别对应三个函数串起来就是一个完整的采集流程。下面按调用顺序写每段都能单独编译测试。4.1 用 SetupAPI 枚举显示器并定位实例路径枚举用SetupDiGetClassDevs类 GUID 用GUID_DEVCLASS_MONITOR标志加DIGCF_PRESENT只取当前在位的设备。遍历时用SetupDiGetDeviceInstanceId拿到实例 ID这个 ID 就是后面拼注册表路径的关键。#include windows.h #include setupapi.h #include devguid.h #include vector #include string #pragma comment(lib, setupapi.lib) // 返回所有在位显示器的设备实例ID形如 DISPLAY\ABC1234\52f7c1a30UID4352 std::vectorstd::wstring EnumMonitorInstanceIds() { std::vectorstd::wstring ids; HDEVINFO h SetupDiGetClassDevsW(GUID_DEVCLASS_MONITOR, nullptr, nullptr, DIGCF_PRESENT); if (h INVALID_HANDLE_VALUE) return ids; SP_DEVINFO_DATA data{}; data.cbSize sizeof(data); for (DWORD i 0; SetupDiEnumDeviceInfo(h, i, data); i) { DWORD size 0; SetupDiGetDeviceInstanceIdW(h, data, nullptr, 0, size); // 先问长度 if (size 0) { std::wstring id(size, L\0); if (SetupDiGetDeviceInstanceIdW(h, data, id[0], size, size)) { id.resize(size - 1); // 去掉结尾的\0 ids.push_back(id); } } } SetupDiDestroyDeviceInfoList(h); // 必须释放否则句柄泄漏 return ids; }逻辑说明SetupDiGetDeviceInstanceIdW调用两次是标准做法第一次传nullptr拿所需缓冲区大小第二次才真正取数据。参数里的size在成功返回后是包含结尾\0的长度所以resize要减 1。SetupDiDestroyDeviceInfoList不能漏循环里反复调用枚举函数时泄漏会很明显。4.2 读取 EDID 二进制值并做长度与校验和检查拿到实例 ID 后拼注册表路径读EDID值。这里有个细节实例 ID 开头的DISPLAY\已经包含在返回值里了拼路径时直接接SYSTEM\CurrentControlSet\Enum\后面不要再补一层。bool ReadEdid(const std::wstring instanceId, std::vectorunsigned char edid) { std::wstring key LSYSTEM\\CurrentControlSet\\Enum\\ instanceId L\\Device Parameters; HKEY hk; if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, key.c_str(), 0, KEY_READ, hk) ! ERROR_SUCCESS) return false; DWORD type 0, size 0; if (RegQueryValueExW(hk, LEDID, nullptr, type, nullptr, size) ! ERROR_SUCCESS || type ! REG_BINARY || size 128) { RegCloseKey(hk); return false; } edid.resize(size); LONG r RegQueryValueExW(hk, LEDID, nullptr, type, edid.data(), size); RegCloseKey(hk); // 无论成功失败都要关 return r ERROR_SUCCESS; }同样的“两次查询”套路第一次拿size同时校验类型必须是REG_BINARY、长度至少 128。长度小于 128 的直接判失败不要试图解析半截数据。读出来之后做一次校验和检查bool EdidChecksumOk(const std::vectorunsigned char e) { if (e.size() 128) return false; unsigned char sum 0; for (int i 0; i 128; i) sum static_castunsigned char(sum e[i]); return sum 0; // 基础块128字节累加应为0 }校验和是 EDID 自带的完整性机制128 字节累加模 256 等于 0 才算合规。但实际环境里虚拟显示器、远程桌面的虚拟屏经常写出不合规的 EDID校验失败时我的做法是记一条警告、继续解析而不是直接丢弃后面第 5 章会细说。4.3 解析成结构体并输出含厂商ID与描述符提取结构体定义把所有要输出的字段集中起来解析函数一处赋值UI 或导出层直接读。struct MonitorInfo { std::string vendor; // 3字母PnP ID unsigned short productCode 0; unsigned int serial 0; int year 0, week 0; int widthCm 0, heightCm 0; double gamma 0.0; std::string name; // 描述符里的显示器名称 std::string serialStr; // 描述符里的序列号字符串 bool checksumOk false; }; MonitorInfo ParseEdid(const std::vectorunsigned char e) { MonitorInfo info; if (e.size() 128) return info; // 头部校验 const unsigned char header[8] {0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00}; if (memcmp(e.data(), header, 8) ! 0) return info; info.checksumOk EdidChecksumOk(e); // 厂商ID5-5-5编码 unsigned short mfg (static_castunsigned short(e[8]) 8) | e[9]; info.vendor.push_back(static_castchar(((mfg 10) 0x1F) A - 1)); info.vendor.push_back(static_castchar(((mfg 5) 0x1F) A - 1)); info.vendor.push_back(static_castchar(( mfg 0x1F) A - 1)); info.productCode static_castunsigned short(e[10] | (e[11] 8)); info.serial e[12] | (e[13] 8) | (e[14] 16) | (e[15] 24); info.week e[16]; info.year e[17] 1990; info.widthCm e[21]; info.heightCm e[22]; info.gamma (e[23] 0xFF) ? 0.0 : (e[23] 100) / 100.0; // 4个描述符块 for (int i 0; i 4; i) { const unsigned char* d e.data() 54 i * 18; if (d[0] || d[1] || d[2]) continue; // 非描述符块跳过 std::string text; for (int k 5; k 18; k) { if (d[k] 0x0A) break; if (d[k] 0x20 d[k] 0x7E) text.push_back(static_castchar(d[k])); } while (!text.empty() text.back() ) text.pop_back(); if (d[3] 0xFC) info.name text; else if (d[3] 0xFF) info.serialStr text; } return info; }逻辑说明先做头部和长度校验不通过直接返回空结构体。厂商 ID 和产品代码按前面讲的偏移取值描述符循环里用d[0] || d[1] || d[2]先过滤掉非描述符块描述符块前 3 字节固定为 0再用d[3]判断类型。0xFC是名称、0xFF是序列号字符串这两类是最常要的其他类型按需加分支即可。gamma 为0xFF时置 0输出层显示“扩展块定义”不要显示成 0 误导人。主函数里把三步串起来控制台逐台打印int wmain() { for (const auto id : EnumMonitorInstanceIds()) { std::vectorunsigned char edid; if (!ReadEdid(id, edid)) continue; MonitorInfo info ParseEdid(edid); printf(%s | %s | %u | %d-%d | %dx%d cm | %s\n, info.vendor.c_str(), info.name.c_str(), info.serial, info.year, info.week, info.widthCm, info.heightCm, info.checksumOk ? ok : checksum-fail); } return 0; }到这里一条完整的采集链路就跑通了。换成 MFC 的话把printf换成往CListCtrl插行解析层一行不用改。5. 排查实录EDID 读取失败的常见现场这套代码逻辑不复杂但在不同机器上跑翻车的方式五花八门。下面几条是我在实际项目里真遇到过的按“现象 → 原因 → 解决”记一下省得下次再查一遍。5.1 打开注册表返回 ERROR_FILE_NOT_FOUND现象EnumMonitorInstanceIds明明返回了非空列表ReadEdid却全部失败RegOpenKeyEx返回 2。原因路径拼接时把Enum\写重复了或者实例 ID 前面多带了\\?\前缀。另一种情况是拿EnumDisplayDevices返回的DeviceID去拼那个字符串格式和 SetupAPI 的实例 ID 不一样不能混用。解决把拼出来的完整路径打印出来对照注册表编辑器手动看一遍。正确的形态是SYSTEM\CurrentControlSet\Enum\DISPLAY\硬件ID\实例ID\Device ParametersDISPLAY只出现一次。5.2 EDID 读到 256 字节显示器名称乱码现象某些机器读出来的size是 256按 128 解析名称时出现乱码或者解析到一串看不懂的字符。原因把扩展块当成基础块解析了。256 字节是“基础块 一个扩展块”扩展块的结构和基础块完全不同偏移 54 往后不是描述符。解决解析函数里只取前 128 字节if (e.size() 128)之后固定按 128 的偏移读。扩展块要单独判断e[126]扩展块数量再走另一套解析逻辑不要混在一个循环里。5.3 校验和不为 0但其他字段看着都正常现象EdidChecksumOk返回 false可厂商、名称、尺寸都解析出来了数据看起来没坏。原因虚拟显示器驱动、远程桌面虚拟屏、部分 KVM 切换器写入的 EDID 本身不合规校验和字节没算对。这种情况在物理显示器上很少见在虚拟环境里很常见。解决校验和只作为“可信度标记”不作为丢弃依据。解析照常进行输出时带一个checksumOk标志让上层决定要不要采信。直接丢弃会导致虚拟屏完全采不到数据反而更麻烦。5.4 多屏机器只拿到一台显示器的信息现象接了双屏程序只输出一条记录。原因SetupDiGetClassDevs的类 GUID 用错了用了显示适配器的类而不是显示器的类或者遍历循环里SP_DEVINFO_DATA没有每次重置cbSize。解决类 GUID 必须是GUID_DEVCLASS_MONITOR。另外SP_DEVINFO_DATA在每次SetupDiEnumDeviceInfo前要保证cbSize已赋值放在循环外初始化一次、循环内不要改但结构体本身每次由 API 填充不要手动清空导致cbSize丢失。5.5 名称描述符尾部带一串空格字符串比较对不上现象解析出的名称是ABC2716Q 拿去做比较永远不相等。原因EDID 名称字段固定 13 字节文本不足时用0x20空格填充很多实现只按 13 字节截断没处理填充。解决循环里遇0x0A立即 break循环结束后再while (!name.empty() name.back() ) pop_back()把尾部空格清干净。这两个动作缺一不可只做其中一个都清不干净。6. 二次开发扩展块解析与多屏映射的改造思路拿到基础块解析之后真正做产品化还要往前推两步。第一步是处理扩展块。支持 HDR 的显示器、4K 高刷屏很多关键参数色域、亮度、色深都在扩展块里。改造时在ParseEdid里判断e[126] 0 e.size() 256然后把e.data() 128传给一个独立的ParseExtensionBlock函数。扩展块偏移 0 是 tag0x02是 CEA-861、0x70是 DisplayID按 tag 分派不要一股脑按 CEA 解。这块我一般先只取 CEA 的“数据块集合”里的视频能力字段够用就行全量解析成本很高。第二步是多屏映射。枚举出来的是“设备实例”但用户关心的是“哪台屏在左边、哪台是主屏”。EnumDisplayDevices能给出适配器与显示器的对应关系EnumDisplaySettings能拿到当前分辨率和刷新率把这三份数据按“显示器设备名”关联起来就能画出“物理位置 → EDID 信息 → 当前模式”的对照表。项目里我是这么串的// 伪代码把EDID信息和当前显示模式关联 for (DWORD i 0; ; i) { DISPLAY_DEVICEW dd{}; dd.cb sizeof(dd); if (!EnumDisplayDevicesW(nullptr, i, dd, 0)) break; // dd.DeviceID 可与SetupAPI实例ID做前缀匹配 DEVMODEW dm{}; dm.dmSize sizeof(dm); if (EnumDisplaySettingsW(dd.DeviceName, ENUM_CURRENT_SETTINGS, dm)) { // dm.dmPelsWidth / dm.dmPelsHeight / dm.dmDisplayFrequency } }匹配时别指望字符串完全相等dd.DeviceID里带的是DISPLAY\...格式和 SetupAPI 实例 ID 只差大小写和前缀做不区分大小写的包含匹配更稳。还有一个容易被忽略的点是缓存。EDID 在显示器不插拔的情况下不会变每次启动都全量枚举注册表在设备多的机器上会拖慢启动。我一般第一次采集后按实例 ID 做内存缓存同时监听WM_DEVICECHANGE里的DBT_DEVICEARRIVAL和DBT_DEVICEREMOVECOMPLETE插拔时只刷新变化的那一台。最后说个血泪教训解析函数里所有对edid数组的访问下标一定要在size()校验之后进行我早期版本图省事在长度检查前就读了e[126]在某台只上报 100 字节的虚拟屏上直接越界崩溃。从那以后我每次改解析逻辑都强制先跑一遍“长度不足 128”和“校验和失败”这两个边界用例再上真机。希望这套拆解能帮你少走点弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑