Windows C++ 显示器信息枚举与显示模式查询实战指南
1. 项目概述与核心价值在Windows桌面应用开发中尤其是涉及到多显示器管理、屏幕录制、远程桌面、游戏全屏模式切换或者系统工具开发时我们经常需要与显示子系统进行深度交互。一个基础且关键的需求就是如何通过程序获取当前系统连接的所有显示器信息以及它们各自支持的显示模式分辨率、刷新率等和当前生效的设置这个需求听起来简单但Windows API在这部分的设计有其历史沿革和复杂性直接上手很容易踩坑。网上能找到的代码片段往往只解决了部分问题或者缺乏关键的错误处理和逻辑解释。今天我们就来彻底拆解这个需求用C和纯Win32 API实现一个健壮的“显示器显示模式枚举与当前设置查询”工具。这不仅仅是调用几个API那么简单我们会深入理解EnumDisplayDevices、EnumDisplaySettingsEx、ChangeDisplaySettingsEx这些函数背后的工作机制搞清楚DEVMODE结构里每个字段的含义并处理多显示器、主副屏、克隆扩展等复杂场景。最终你会得到一个可以直接集成到项目中的、带有完整错误处理和详细注释的C源码模块。2. 核心API与数据结构深度解析在动手写代码之前我们必须先理解Windows显示子系统暴露给我们的几个核心武器。如果对这些API和数据结构一知半解写出来的代码就会非常脆弱。2.1 显示器设备枚举EnumDisplayDevices这个函数是我们的起点用于获取系统识别到的物理显示设备列表。注意这里的“设备”可能包括真实的显示器也可能包括虚拟显示驱动创建的设备。BOOL EnumDisplayDevicesW( LPCWSTR lpDevice, // 设备名首次调用通常为NULL DWORD iDevNum, // 设备索引号从0开始 PDISPLAY_DEVICEW lpDisplayDevice, // 输出设备信息 DWORD dwFlags // 保留必须为0 );关键点在于DISPLAY_DEVICE这个结构体。我们最需要关注的字段是DeviceName: 设备的唯一标识符如\\.\DISPLAY1后续所有针对特定显示器的操作都需要用到它。DeviceString: 设备的友好名称如“Generic PnP Monitor”或显卡型号。StateFlags: 一组标志位这是信息金矿。DISPLAY_DEVICE_ATTACHED_TO_DESKTOP: 该设备当前是否连接到桌面即正在使用。一个未被启用的显示器或投影仪可能没有这个标志。DISPLAY_DEVICE_PRIMARY_DEVICE: 该设备是否是主显示器。DISPLAY_DEVICE_MIRRORING_DRIVER: 该设备是否由镜像驱动创建用于远程桌面、某些录屏软件。DISPLAY_DEVICE_VGA_COMPATIBLE: 是否兼容VGA。实操心得1EnumDisplayDevices需要循环调用。第一次调用时lpDevice传NULLiDevNum从0递增直到函数返回FALSE。但这里有个坑某些虚拟设备或驱动可能会被枚举出来但其StateFlags里没有ATTACHED_TO_DESKTOP。在开发多显示器管理工具时我们通常只关心那些“已连接桌面”的设备。2.2 显示模式枚举与查询EnumDisplaySettingsEx获取到具体的显示器设备名后我们就可以查询它支持的所有显示模式或者获取它当前的设置。这是通过EnumDisplaySettingsEx函数完成的。BOOL EnumDisplaySettingsExW( LPCWSTR lpszDeviceName, // 设备名来自DISPLAY_DEVICE.DeviceName DWORD iModeNum, // 模式索引或特殊值如ENUM_CURRENT_SETTINGS DEVMODEW *lpDevMode, // 输出显示模式信息 DWORD dwFlags // 标志如EDS_RAWMODE );iModeNum参数是关键。当它等于ENUM_CURRENT_SETTINGS(-1) 时函数会获取该显示器当前正在使用的显示模式。当它等于ENUM_REGISTRY_SETTINGS(-2) 时获取的是存储在注册表中的、该显示器的持久化设置通常是用户上次注销或重启前的设置。当它是一个从0开始的递增索引时则用于枚举该显示器支持的所有模式。DEVMODE结构体包含了显示模式的所有细节。我们必须将dmSize初始化为sizeof(DEVMODE)这是许多新手容易忘记导致崩溃的地方。重要字段包括dmPelsWidth,dmPelsHeight: 分辨率宽、高。dmDisplayFrequency: 刷新率Hz。dmBitsPerPel: 颜色深度每像素位数如32。dmPosition.x,dmPosition.y: 该显示器在虚拟桌面坐标系中的位置对于多显示器布局至关重要。dmDisplayFlags: 可能包含DM_INTERLACED标志表示隔行扫描。dmFields: 一个位掩码指示DEVMODE中哪些字段是有效/被设置的。在调用ChangeDisplaySettingsEx修改设置时必须正确设置此字段。实操心得2枚举所有支持的模式也是一个循环过程从iModeNum 0开始直到函数返回FALSE。但要注意不同显卡驱动报告的模式列表可能包含大量重复或非常相似的模式比如同一分辨率下细微的刷新率差异。在实际应用中我们通常需要对枚举出的模式进行去重和排序以提供一个清晰的列表给用户选择。2.3 显示设置变更ChangeDisplaySettingsEx虽然我们本次项目的核心是“查询”但理解如何“设置”能帮助我们更好地理解整个流程。ChangeDisplaySettingsEx函数用于动态改变显示器的设置。LONG ChangeDisplaySettingsExW( LPCWSTR lpszDeviceName, // 设备名NULL表示所有显示器 DEVMODEW *lpDevMode, // 新的显示模式NULL表示使用注册表设置 HWND hwnd, // 保留必须为NULL DWORD dwflags, // 标志如CDS_UPDATEREGISTRY LPVOID lParam // 保留必须为NULL );dwflags参数中的CDS_UPDATEREGISTRY标志非常重要。如果设置了这个标志变更不仅会立即生效还会被写入注册表成为新的持久化设置。如果不设置则变更仅在本次会话中有效临时测试分辨率时常用。返回值DISP_CHANGE_SUCCESSFUL表示成功。其他返回值如DISP_CHANGE_BADMODE不支持的模式、DISP_CHANGE_NOTUPDATED无法写入注册表等都需要我们妥善处理。注意在程序中进行显示模式切换尤其是降低分辨率或刷新率是高风险操作可能导致屏幕黑屏、闪烁甚至系统暂时无响应。务必在变更前保存当前的DEVMODE并提供一种恢复机制例如在变更失败或用户无操作一段时间后自动恢复原设置。对于面向普通用户的应用程序应谨慎使用此功能并给出明确警告。3. 项目设计与模块化实现理解了核心API后我们来设计代码结构。一个好的设计应该将数据获取、处理和展示分离。我们将创建几个核心的类和函数。3.1 数据结构封装DisplayInfo与DisplayMode首先我们定义两个结构体来封装显示器和显示模式的信息这样比直接使用原始的Windows结构体更清晰、更安全。// DisplayMode.h #pragma once #include string #include cstdint struct DisplayMode { int width 0; int height 0; int refreshRate 0; // Hz int bitsPerPixel 0; // 通常是32 bool isInterlaced false; bool isCurrent false; // 是否是当前使用的模式 bool isPreferred false; // 是否是驱动报告的首选模式通常对应原生分辨率 // 用于去重和排序的比较运算符 bool operator(const DisplayMode other) const { return width other.width height other.height refreshRate other.refreshRate bitsPerPixel other.bitsPerPixel; } bool operator(const DisplayMode other) const { if (width ! other.width) return width other.width; if (height ! other.height) return height other.height; if (refreshRate ! other.refreshRate) return refreshRate other.refreshRate; return bitsPerPixel other.bitsPerPixel; } std::wstring ToString() const { wchar_t buffer[256]; swprintf_s(buffer, L%dx%d %d Hz, %d-bit%s%s, width, height, refreshRate, bitsPerPixel, isInterlaced ? L (Interlaced) : L, isCurrent ? L [Current] : L, isPreferred ? L [Preferred] : L); return std::wstring(buffer); } };// DisplayInfo.h #pragma once #include string #include vector #include DisplayMode.h struct DisplayInfo { std::wstring deviceName; // 例如\\.\DISPLAY1 std::wstring friendlyName; // 例如BenQ PD2700U std::wstring adapterName; // 显卡适配器名称 bool isPrimary false; bool isAttachedToDesktop false; bool isMirroringDriver false; // 显示器在虚拟桌面中的位置和大小基于当前设置 int desktopPosX 0; int desktopPosY 0; int desktopWidth 0; int desktopHeight 0; // 该显示器支持的所有显示模式 std::vectorDisplayMode supportedModes; // 当前正在使用的显示模式在supportedModes中会有一个对应的isCurrent为true的项 DisplayMode currentMode; // 注册表中保存的持久化设置 DisplayMode registryMode; std::wstring ToString() const { wchar_t buffer[512]; swprintf_s(buffer, LDevice: %s\nFriendly: %s\nPrimary: %s, Attached: %s\nPosition: (%d, %d) Size: %dx%d\nCurrent Mode: %s, deviceName.c_str(), friendlyName.c_str(), isPrimary ? LYes : LNo, isAttachedToDesktop ? LYes : LNo, desktopPosX, desktopPosY, desktopWidth, desktopHeight, currentMode.ToString().c_str()); return std::wstring(buffer); } };3.2 核心功能类DisplayManager我们将所有与Windows API交互的逻辑封装在一个DisplayManager类中。这个类负责枚举设备、查询模式并处理可能发生的错误。// DisplayManager.h #pragma once #include vector #include memory #include DisplayInfo.h class DisplayManager { public: DisplayManager(); ~DisplayManager() default; // 主入口刷新并获取所有显示器信息 bool RefreshDisplayInformation(); // 获取所有已连接桌面的显示器 const std::vectorDisplayInfo GetActiveDisplays() const { return m_activeDisplays; } // 获取所有枚举到的设备包括未连接的 const std::vectorDisplayInfo GetAllDisplayDevices() const { return m_allDevices; } // 根据设备名查找显示器信息实用函数 const DisplayInfo* FindDisplayByDeviceName(const std::wstring deviceName) const; // 静态工具函数将DEVMODE转换为我们的DisplayMode结构 static DisplayMode DevModeToDisplayMode(const DEVMODEW devMode); private: // 内部实现函数 bool EnumerateAllDevices(); bool QueryModesForDevice(DisplayInfo info); bool GetCurrentAndRegistrySettings(DisplayInfo info); std::vectorDisplayInfo m_allDevices; std::vectorDisplayInfo m_activeDisplays; // 仅包含 isAttachedToDesktop 的设备 };3.3 关键实现细节与错误处理下面是DisplayManager类部分核心函数的实现其中包含了大量的细节和错误处理逻辑。// DisplayManager.cpp #include DisplayManager.h #include Windows.h #include algorithm #include iostream // 用于调试输出实际项目中可替换为日志库 DisplayManager::DisplayManager() { // 构造函数中可以初始化一些资源目前不需要 } bool DisplayManager::RefreshDisplayInformation() { m_allDevices.clear(); m_activeDisplays.clear(); if (!EnumerateAllDevices()) { std::wcerr LFailed to enumerate display devices. std::endl; return false; } // 为每个已连接桌面的设备查询详细信息 for (auto device : m_allDevices) { if (device.isAttachedToDesktop) { if (QueryModesForDevice(device)) { if (GetCurrentAndRegistrySettings(device)) { m_activeDisplays.push_back(device); } } } } return !m_activeDisplays.empty(); } bool DisplayManager::EnumerateAllDevices() { DISPLAY_DEVICEW dd; dd.cb sizeof(DISPLAY_DEVICEW); DWORD deviceIndex 0; while (EnumDisplayDevicesW(nullptr, deviceIndex, dd, 0)) { DisplayInfo info; info.deviceName dd.DeviceName; info.friendlyName dd.DeviceString; info.adapterName dd.DeviceID; // DeviceID通常包含适配器信息 info.isPrimary (dd.StateFlags DISPLAY_DEVICE_PRIMARY_DEVICE) ! 0; info.isAttachedToDesktop (dd.StateFlags DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) ! 0; info.isMirroringDriver (dd.StateFlags DISPLAY_DEVICE_MIRRORING_DRIVER) ! 0; // 注意EnumDisplayDevices可能不会返回桌面位置信息。 // 我们需要通过EnumDisplaySettingsEx获取当前设置来得到位置。 m_allDevices.push_back(info); // 重要需要清零结构体以备下次循环使用 ZeroMemory(dd, sizeof(dd)); dd.cb sizeof(dd); deviceIndex; } DWORD lastError GetLastError(); if (lastError ! ERROR_SUCCESS lastError ! ERROR_NO_MORE_ITEMS) { std::wcerr LEnumDisplayDevices failed with error: lastError std::endl; return false; } return true; } bool DisplayManager::QueryModesForDevice(DisplayInfo info) { info.supportedModes.clear(); DEVMODEW dm {}; dm.dmSize sizeof(DEVMODEW); DWORD modeIndex 0; // 枚举所有支持的模式 while (EnumDisplaySettingsExW(info.deviceName.c_str(), modeIndex, dm, EDS_RAWMODE)) { DisplayMode mode DevModeToDisplayMode(dm); info.supportedModes.push_back(mode); modeIndex; // 重置dm安全起见 ZeroMemory(dm, sizeof(dm)); dm.dmSize sizeof(dm); } // 对模式列表进行去重和排序提升用户体验 std::sort(info.supportedModes.begin(), info.supportedModes.end()); auto last std::unique(info.supportedModes.begin(), info.supportedModes.end()); info.supportedModes.erase(last, info.supportedModes.end()); return !info.supportedModes.empty(); } bool DisplayManager::GetCurrentAndRegistrySettings(DisplayInfo info) { DEVMODEW dmCurrent {}, dmRegistry {}; dmCurrent.dmSize sizeof(DEVMODEW); dmRegistry.dmSize sizeof(DEVMODEW); // 获取当前设置 if (!EnumDisplaySettingsExW(info.deviceName.c_str(), ENUM_CURRENT_SETTINGS, dmCurrent, 0)) { std::wcerr LFailed to get current settings for device: info.deviceName std::endl; return false; } info.currentMode DevModeToDisplayMode(dmCurrent); info.currentMode.isCurrent true; // 获取注册表设置 if (EnumDisplaySettingsExW(info.deviceName.c_str(), ENUM_REGISTRY_SETTINGS, dmRegistry, 0)) { info.registryMode DevModeToDisplayMode(dmRegistry); } else { // 获取注册表设置失败可能发生在某些虚拟显示器上这不是致命错误 info.registryMode info.currentMode; // 降级处理 } // 从当前设置中提取桌面位置和尺寸 info.desktopPosX dmCurrent.dmPosition.x; info.desktopPosY dmCurrent.dmPosition.y; info.desktopWidth dmCurrent.dmPelsWidth; info.desktopHeight dmCurrent.dmPelsHeight; // 标记首选模式通常是最高的分辨率或者由驱动指定这里我们简单标记与注册表设置一致的模式 for (auto mode : info.supportedModes) { if (mode info.registryMode) { mode.isPreferred true; } if (mode info.currentMode) { mode.isCurrent true; } } return true; } DisplayMode DisplayManager::DevModeToDisplayMode(const DEVMODEW devMode) { DisplayMode mode; mode.width devMode.dmPelsWidth; mode.height devMode.dmPelsHeight; mode.refreshRate devMode.dmDisplayFrequency; mode.bitsPerPixel devMode.dmBitsPerPel; mode.isInterlaced (devMode.dmDisplayFlags DM_INTERLACED) ! 0; return mode; } const DisplayInfo* DisplayManager::FindDisplayByDeviceName(const std::wstring deviceName) const { auto it std::find_if(m_activeDisplays.begin(), m_activeDisplays.end(), [deviceName](const DisplayInfo info) { return info.deviceName deviceName; }); if (it ! m_activeDisplays.end()) { return (*it); } return nullptr; }4. 使用示例与结果展示有了DisplayManager类在主程序中使用就变得非常简单。下面是一个控制台程序的示例它打印出所有显示器的详细信息。// main.cpp #include DisplayManager.h #include iostream #include iomanip int main() { // 设置控制台输出为UTF-8以正确显示宽字符在Windows上可能需要额外配置 std::ios_base::sync_with_stdio(false); std::locale::global(std::locale()); std::wcout.imbue(std::locale()); DisplayManager dm; std::wcout L正在枚举显示器信息... std::endl std::endl; if (!dm.RefreshDisplayInformation()) { std::wcerr L无法获取显示器信息。程序退出。 std::endl; return 1; } const auto displays dm.GetActiveDisplays(); std::wcout L找到 displays.size() L 个已连接的显示器 std::endl; std::wcout L std::endl; for (size_t i 0; i displays.size(); i) { const auto disp displays[i]; std::wcout L\n[显示器 (i 1) L] std::endl; std::wcout L----------------------------------------- std::endl; std::wcout L设备标识: disp.deviceName std::endl; std::wcout L友好名称: disp.friendlyName std::endl; std::wcout L主显示器: (disp.isPrimary ? L是 : L否) std::endl; std::wcout L桌面位置: ( disp.desktopPosX L, disp.desktopPosY L) std::endl; std::wcout L桌面区域: disp.desktopWidth L x disp.desktopHeight std::endl; std::wcout L当前模式: disp.currentMode.ToString() std::endl; std::wcout L注册表模式: disp.registryMode.ToString() std::endl; std::wcout L\n支持的显示模式 ( disp.supportedModes.size() L 个): std::endl; for (const auto mode : disp.supportedModes) { std::wcout L - mode.ToString() std::endl; } } std::wcout L\n std::endl; std::wcout L枚举完成。按回车键退出... std::endl; std::cin.get(); return 0; }编译并运行这个程序确保链接了user32.lib因为Windows显示API在其中你将会在控制台看到类似下面的输出正在枚举显示器信息... 找到 2 个已连接的显示器 [显示器 1] ----------------------------------------- 设备标识: \\.\DISPLAY1 友好名称: NVIDIA GeForce RTX 3060 主显示器: 是 桌面位置: (0, 0) 桌面区域: 2560 x 1440 当前模式: 2560x1440 144 Hz, 32-bit [Current] 注册表模式: 2560x1440 144 Hz, 32-bit [Preferred] 支持的显示模式 (15 个): - 640x480 60 Hz, 32-bit - 800x600 60 Hz, 32-bit - 1024x768 60 Hz, 32-bit - 1280x720 60 Hz, 32-bit - 1280x800 60 Hz, 32-bit - 1920x1080 60 Hz, 32-bit - 2560x1440 60 Hz, 32-bit - 2560x1440 120 Hz, 32-bit - 2560x1440 144 Hz, 32-bit [Current] [Preferred] [显示器 2] ----------------------------------------- 设备标识: \\.\DISPLAY2 友好名称: Dell U2415 主显示器: 否 桌面位置: (2560, 300) // 注意主显示器右侧并向下偏移了300像素 桌面区域: 1920 x 1200 当前模式: 1920x1200 60 Hz, 32-bit [Current] 注册表模式: 1920x1200 60 Hz, 32-bit [Preferred] ...这个输出清晰地展示了每个显示器的关键信息、它们在虚拟桌面中的布局关系以及所有可用的显示模式。5. 常见问题、陷阱与排查技巧在实际开发和使用过程中你几乎一定会遇到下面这些问题。这里我把自己踩过的坑和解决方案总结出来。5.1 问题一EnumDisplayDevices返回了不存在的或未连接的显示器现象枚举出的设备数量多于物理连接的显示器其中一些设备的StateFlags里没有DISPLAY_DEVICE_ATTACHED_TO_DESKTOP标志。原因系统可能保留了之前连接过的显示器的信息或者存在虚拟显示驱动如用于远程桌面的RDPDD Chained DD驱动。解决方案在业务逻辑中始终以DISPLAY_DEVICE_ATTACHED_TO_DESKTOP标志作为判断显示器是否“可用”或“活跃”的主要依据。我们的DisplayManager类已经做了这个区分m_activeDisplays只包含已连接的设备。5.2 问题二EnumDisplaySettingsEx枚举模式时返回重复项或顺序混乱现象同一个分辨率/刷新率组合出现了多次或者列表顺序不符合用户习惯。原因显卡驱动可能会报告多个相同的模式或者以驱动内部的顺序返回。解决方案在QueryModesForDevice函数中我们使用了std::sort和std::unique对模式列表进行去重和排序。排序规则在DisplayMode::operator中定义是先按宽度、再按高度、然后按刷新率排序这符合大多数用户的浏览习惯。5.3 问题三获取到的刷新率是0现象某些显示模式尤其是低分辨率模式的dmDisplayFrequency字段为0。原因对于某些老旧的显示模式或驱动Windows可能无法获取或报告有效的刷新率0表示“使用硬件默认值”。处理建议在UI上显示刷新率时如果值为0可以显示为“默认”或“N/A”而不是直接显示0 Hz避免用户困惑。5.4 问题四在多显示器系统上ChangeDisplaySettingsEx行为异常现象试图修改一个显示器的设置却影响了其他显示器或者修改失败。原因当lpDevMode参数中的dmPosition字段被设置且新的位置与现有显示器的桌面区域不连续或重叠时Windows可能会尝试重新排列所有显示器以满足新的布局这可能导致意外的全局变更。关键技巧如果你只想修改单个显示器的分辨率或刷新率而不想改变其在桌面中的位置那么在调用ChangeDisplaySettingsEx之前必须先从当前设置ENUM_CURRENT_SETTINGS中获取该显示器的dmPosition值并将其设置到新的DEVMODE中。同时确保dmFields掩码包含了DM_POSITION。这样Windows就知道你只想修改其他参数而保持位置不变。// 示例安全地修改显示器分辨率保持其位置不变 bool SafeChangeResolution(const std::wstring deviceName, int newWidth, int newHeight, int newRefreshRate) { DEVMODEW dmCurrent {}; dmCurrent.dmSize sizeof(DEVMODEW); if (!EnumDisplaySettingsExW(deviceName.c_str(), ENUM_CURRENT_SETTINGS, dmCurrent, 0)) { return false; } DEVMODEW dmNew dmCurrent; // 复制当前设置包括位置 dmNew.dmPelsWidth newWidth; dmNew.dmPelsHeight newHeight; dmNew.dmDisplayFrequency newRefreshRate; dmNew.dmFields DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY | DM_POSITION; // 必须包含DM_POSITION LONG result ChangeDisplaySettingsExW(deviceName.c_str(), dmNew, NULL, 0, NULL); return (result DISP_CHANGE_SUCCESSFUL); }5.5 问题五程序在远程桌面或虚拟机中运行结果不一致现象在物理机上运行正常的程序在通过远程桌面RDP连接或虚拟机VMware, VirtualBox中运行时枚举到的显示器信息不同可能只有一个虚拟显示器或者分辨率列表有限。原因远程桌面和虚拟机使用虚拟显示驱动来模拟显示器。这些驱动通常只暴露有限的、与远程会话或虚拟机窗口大小相关的显示模式。应对策略检查DISPLAY_DEVICE的StateFlags是否包含DISPLAY_DEVICE_MIRRORING_DRIVER或DISPLAY_DEVICE_REMOTE。对于这类环境你的程序应该优雅降级例如不提供修改分辨率的功能或者只显示虚拟驱动提供的有限选项并给出友好提示“当前处于远程会话中显示设置受限”。6. 进阶应用与扩展思路掌握了基础的枚举和查询后这个核心模块可以成为许多实用工具的基础。1. 多显示器布局可视化工具利用desktopPosX/Y和desktopWidth/Height你可以绘制一个简单的示意图显示所有显示器在虚拟桌面上的相对位置和大小帮助用户直观理解“扩展”显示是如何工作的。2. 显示配置一键切换器开发一个托盘程序允许用户保存多套显示配置例如“在家办公双屏扩展”、“会议室演示仅第二屏幕复制”、“单屏模式”并快速切换。这需要组合使用EnumDisplaySettingsEx获取当前设置、ChangeDisplaySettingsEx应用新设置以及注册表或配置文件来保存方案。3. 游戏全屏模式辅助工具许多游戏在全屏运行时会切换到显示器支持的最高刷新率。你可以写一个工具在启动游戏前自动将主显示器切换到游戏所需的最佳分辨率和高刷新率模式游戏退出后再恢复原设置。这能提升游戏体验并避免用户手动切换的麻烦。4. 显示器信息诊断工具将获取到的信息EDID数据可以通过SetupAPI进一步获取更详细地展示出来例如制造商、型号、序列号、支持的颜色空间sRGB, DCI-P3、HDR支持等用于硬件排查或专业调色。实现这些扩展功能本质上都是对我们今天构建的DisplayManager类的进一步封装和调用。当你深刻理解了DEVMODE、DISPLAY_DEVICE以及那几个关键API后这些扩展就变成了按图索骥的工程实践。