在Windows系统的显示器设置中,微软为我们提供了如下的设置界面,来完成多屏幕的显示方案定制。比如设置多个屏幕的空间排列和显示方向、设置每个屏幕的分辨率大小、调整主显示屏幕、以及调整多个屏幕的协作能力是复制还是扩展。那么在此基础上,如何用代码自动化设置一个固定显示设置方案?下面将从基础API接口详细介绍显示器设置如何自定义。

图1 - 系统显示器设置

  随着Windows历史发展,Windows发展出了两套不兼容的显示驱动模型。其分别是 Windows 2000/XP 以 NT5 为内核的XDDM驱动模型,以及后来的 Windows Vista/7/8/10 这些以 NT6 为内核的WDDM驱动模型。在此过程中屏幕相关的设置API也发展出了两代,其中 XDDM 模型的 API 接口主要以 DEVMODE 结构为主,WDDM模型的 API 接口主要以 DISPLAYCONFIG 为前缀的结构为主。

一、 XDDM

  由于XDDM模型主要是为2000/XP等老式系统设计,在此我们不主要使用该模式的下接口。但该模式下的 API 接口有助于帮助我们理解Windows的显示模型。所以在此提出,该模型下主要提供以下几个主要接口函数:

  • EnumDisplayDevices
  • EnumDisplayMonitors
  • EnumDisplaySettings
  • EnumDisplaySettingsEx
  • ChangeDisplaySettings
  • ChangeDisplaySettingsEx
  • GetMonitorInfo

  在此我们主要使用 EnumDisplayDevices 接口函数,该函数的原型定义如下:

BOOL EnumDisplayDevicesW(
  [in]  LPCWSTR          lpDevice,
  [in]  DWORD            iDevNum,
  [out] PDISPLAY_DEVICEW lpDisplayDevice,
  [in]  DWORD            dwFlags
);

1、 枚举适配器信息

  依据该函数的官方 Remark 说明,不指定 lpDevice,并递增 iDevNum 做循环枚举操作,来取得显示适配器的信息。

void XDDM::PrintAdapter(std::vector<DISPLAY_DEVICE>& devices)
{
    devices.clear();
    int index = 0;

    while (true)
    {
        DISPLAY_DEVICE dev{};
        dev.cb = sizeof(DISPLAY_DEVICE);
        if (EnumDisplayDevices(nullptr, index, &dev, 0)) //使用nullptr取得显示卡适配器信息,也可用适配器名称进而取得显示器信息
        {
            if (!(dev.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER))
            {
                devices.push_back(dev);
                bool active = dev.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP; //当前接口是否启用了

                //do something
            }

            ++index;
            WLOG(L"ADAPTER==> \t devName:%s \t devId:%s \r\n", dev.DeviceName, dev.DeviceID);
        }
        else
            break;
    }
}

  通过上述操作我们可以枚举到如下的显示适配器信息,所谓的显示适配器其实就是GPU显卡。在此我们需要理解一个概念即 "同一适配器上可挂接多个监视器",换句话说就是当前系统上有多少个硬件接口,系统就会注册对应数量的显示器编号 \\.\DISPLAY*

ADAPTER==>       devName:\\.\DISPLAY1    devId:PCI\VEN_8086&DEV_0412&SUBSYS_85341043&REV_06
ADAPTER==>       devName:\\.\DISPLAY2    devId:PCI\VEN_8086&DEV_0412&SUBSYS_85341043&REV_06
ADAPTER==>       devName:\\.\DISPLAY3    devId:PCI\VEN_8086&DEV_0412&SUBSYS_85341043&REV_06

  从设备信息打印中可看到 devId 都相同,而不同的三个 devName 接口分别注册在系统注册表中,分别对应的是主板上的三个显示器插槽(DVI、HDMI、VGA)。该解释无从考证,但我们可以通过CPU规格书中的“显示器数量”和“图形输出”信息,以及主板的插槽设计佐证这一点。

2、 枚举监视器信息

  枚举到适配器信息后,以适配器名称传参至 lpDevice,并递增 iDevNum 再次做循环枚举操作,来取得显示监视器的信息。

void XDDM::PrintMonitor(std::vector<DISPLAY_DEVICE>& devices)
{
    bool find = false;

    if (!devices.empty())
    {
        for (auto dev = devices.begin(); dev != devices.end();)
        {
            int index = 0;
            while (true)
            {
                DISPLAY_DEVICE tempDev{};
                tempDev.cb = sizeof(DISPLAY_DEVICE);
                if (EnumDisplayDevices(dev->DeviceName, index, &tempDev, EDD_GET_DEVICE_INTERFACE_NAME))
                {
                    bool active = tempDev.StateFlags & DISPLAY_DEVICE_ACTIVE;
                    //do something

                    ++index;
                    WLOG(L"MONITOR==> \t devName:%s \t active:%d \t attached:%d \ndevId:%s \r\n\n"
                         , tempDev.DeviceName
                         , tempDev.StateFlags & DISPLAY_DEVICE_ACTIVE
                         , tempDev.StateFlags & DISPLAY_DEVICE_ATTACHED
                         , tempDev.DeviceID
                    );
                }
                else
                {
                    ++dev; //goto next
                    break;
                }
            }
        }
    }
}

  通过以上操作可取得如下的详细监视器信息,注意:

MONITOR==>       devName:\\.\DISPLAY1\Monitor0   active:1        attached:2
devId:\\?\DISPLAY#TCL0000#4&35aefc14&0&UID50725632#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

MONITOR==>       devName:\\.\DISPLAY2\Monitor0   active:1        attached:2
devId:\\?\DISPLAY#HKC21A6#4&35aefc14&0&UID16843008#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

  从上方的监视器信息打印中可了解一个基础信息,监视器挂接在适配器端口下。上面这些基础打印信息有助于我们使用WDDM模型接口函数。

二、 WDDM

  WDDM是我们实现自定义的设置的主要接口模型,该接口模型下主要提供以下接口函数:

  • DisplayConfigGetDeviceInfo
  • DisplayConfigSetDeviceInfo
  • GetDisplayConfigBufferSizes
  • QueryDisplayConfig
  • SetDisplayConfig

  需要注意是的 GetDisplayConfigBufferSizesQueryDisplayConfigSetDisplayConfig 这几个函数要求程序是桌面程序,拥有访问控制台会话或桌面能力否者将其返回 ERROR_ACCESS_DENIED

  在WDDM模型中,微软简化了配置方式。抽象出了两个概念 PATHMODE,后续的所有操作均围绕这两个概念展开。其中 MODE 相关的原型结构定义如下:

typedef struct DISPLAYCONFIG_SOURCE_MODE
{
    UINT32                      width;
    UINT32                      height;
    DISPLAYCONFIG_PIXELFORMAT   pixelFormat;
    POINTL                      position;
} DISPLAYCONFIG_SOURCE_MODE;

typedef struct DISPLAYCONFIG_TARGET_MODE
{
    DISPLAYCONFIG_VIDEO_SIGNAL_INFO   targetVideoSignalInfo;
} DISPLAYCONFIG_TARGET_MODE;

typedef struct DISPLAYCONFIG_MODE_INFO
{
    DISPLAYCONFIG_MODE_INFO_TYPE    infoType;
    UINT32                          id;
    LUID                            adapterId;
    union
    {
        DISPLAYCONFIG_TARGET_MODE   targetMode;
        DISPLAYCONFIG_SOURCE_MODE   sourceMode;
    };
} DISPLAYCONFIG_MODE_INFO;

  而 PATH 相关的原型结构定义如下:

typedef struct DISPLAYCONFIG_PATH_SOURCE_INFO
{
    LUID    adapterId;
    UINT32  id;
    UINT32  modeInfoIdx;
    UINT32  statusFlags;
} DISPLAYCONFIG_PATH_SOURCE_INFO;

typedef struct DISPLAYCONFIG_PATH_TARGET_INFO
{
    LUID                                    adapterId;
    UINT32                                  id;
    UINT32                                  modeInfoIdx;
    DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY   outputTechnology;
    DISPLAYCONFIG_ROTATION                  rotation;
    DISPLAYCONFIG_SCALING                   scaling;
    DISPLAYCONFIG_RATIONAL                  refreshRate;
    DISPLAYCONFIG_SCANLINE_ORDERING         scanLineOrdering;
    BOOL                                    targetAvailable;
    UINT32                                  statusFlags;
} DISPLAYCONFIG_PATH_TARGET_INFO;

typedef struct DISPLAYCONFIG_PATH_INFO
{
    DISPLAYCONFIG_PATH_SOURCE_INFO  sourceInfo;
    DISPLAYCONFIG_PATH_TARGET_INFO  targetInfo;
    UINT32                          flags;
} DISPLAYCONFIG_PATH_INFO;

  从定义中有些难以理解这两者之间关系,我们通过一些操作将其打印出实际数据,更方便观察其两者之间的相互关系。

1、 如何设置显示模式

  要想设置显示模式则其主要操作是构建一个 PATHMODE 的匹配对列。并最终交由函数 SetDisplayConfig 执行设置操作。要实现这一过程则先需要弄清楚这两者之前的匹配关系。

1.1 打印 PATH 与 MODE 的数据

  首先我们定义一个辅助结构如下:

typedef struct _DISPLAYCONFIG_SETTING_INFO
{
    std::shared_ptr<DISPLAYCONFIG_PATH_INFO> pathPtr;
    size_t pathCount;
    std::shared_ptr<DISPLAYCONFIG_MODE_INFO> modePtr;
    size_t modeCount;
} DISPLAYCONFIG_SETTING_INFO;

  依据微软官方文档搭配 GetDisplayConfigBufferSizesQueryDisplayConfig 这两接口函数取得当前系统内的显示设置。

bool WDDM::GetPathModeInfo(DISPLAYCONFIG_SETTING_INFO& setInfo, bool queryAll)
{
    //这几个函数 GetDisplayConfigBufferSizes / QueryDisplayConfig / SetDisplayConfig
    //要求程序是桌面程序,拥有访问控制台会话或桌面否者将其返回 ERROR_ACCESS_DENIED

    if (GetDisplayConfigBufferSizes(queryAll ? QDC_ALL_PATHS : QDC_ONLY_ACTIVE_PATHS, &setInfo.pathCount,
        &setInfo.modeCount) == ERROR_SUCCESS)
    {
        setInfo.pathPtr = std::shared_ptr<DISPLAYCONFIG_PATH_INFO>(new DISPLAYCONFIG_PATH_INFO[setInfo.pathCount]{}, std::default_delete<DISPLAYCONFIG_PATH_INFO[]>{});
        setInfo.modePtr = std::shared_ptr<DISPLAYCONFIG_MODE_INFO>(new DISPLAYCONFIG_MODE_INFO[setInfo.modeCount]{}, std::default_delete<DISPLAYCONFIG_MODE_INFO[]>{});

        return QueryDisplayConfig(queryAll ? QDC_ALL_PATHS : QDC_ONLY_ACTIVE_PATHS
            , &setInfo.pathCount
            , setInfo.pathPtr.get()
            , &setInfo.modeCount
            , setInfo.modePtr.get()
            , nullptr) == ERROR_SUCCESS;
    }
    return false;
}

  此时取得的 PATH 和 MODE 信息还比较简陋,还需要继续对其数据进行查询。为此我很还需要再定义一个带有扩展的数据的 DISPLAYCONFIG_PATH_INFO_EX如下:

typedef struct _PATH_INFO_EX
{
    DISPLAYCONFIG_SOURCE_MODE srcMode;

    //DISPLAYCONFIG_SOURCE_DEVICE_NAME
    wchar_t srcDevName[32];

    //DISPLAYCONFIG_TARGET_PREFERRED_MODE
    unsigned int dstWidth;
    unsigned int dstHeight;
    DISPLAYCONFIG_TARGET_MODE dstMode;

    //DEVMODE devMode{};
    //devMode.dmSize = sizeof(DEVMODE);
    //EnumDisplaySettingsEx(srcDevName, ENUM_REGISTRY_SETTINGS, &devMode, EDS_RAWMODE))
    DISPLAYCONFIG_PIXELFORMAT dstPixelFormat;

    //DISPLAYCONFIG_ADAPTER_NAME
    wchar_t srcAdapterPath[128];
    wchar_t dstAdapterPath[128];

    //DISPLAYCONFIG_TARGET_DEVICE_NAME
    wchar_t dstMonitorName[64];
    wchar_t dstMonitorPath[128];
    DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY dstOutputTechnology;
} PATH_INFO_EX;

typedef struct _DISPLAYCONFIG_PATH_INFO_EX
{
    DISPLAYCONFIG_PATH_INFO path;
    PATH_INFO_EX ex;
} DISPLAYCONFIG_PATH_INFO_EX;

  基于 DisplayConfigGetDeviceInfo 接口函数,依据不同参数类型取得该 PATH 的其他可用信息并将其存储到一个新的PATH队列。其中在查询过程对其 PATH 的有效性做过滤,对于查询不到监视器的设备路径的 PATH 应当认定为没有物理连接从而对其丢弃。该操作实现如下:

bool WDDM::FillInfoEx(DISPLAYCONFIG_SETTING_INFO &setInfo, DISPLAYCONFIG_PATH_INFO_EX& infoEx, bool &isDefault)
{
    LUID& srcAdapterId = infoEx.path.sourceInfo.adapterId;
    LUID& dstAdapterId = infoEx.path.targetInfo.adapterId;
    UINT& srcId = infoEx.path.sourceInfo.id;
    UINT& dstId = infoEx.path.targetInfo.id;

    DISPLAYCONFIG_TARGET_DEVICE_NAME targetName{};
    targetName.header.size = sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME);
    targetName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
    targetName.header.adapterId = dstAdapterId;
    targetName.header.id = dstId;
    if (DisplayConfigGetDeviceInfo(&targetName.header) == ERROR_SUCCESS)
    {
        infoEx.ex.dstOutputTechnology = targetName.outputTechnology;
        wcscpy_s(infoEx.ex.dstMonitorPath, targetName.monitorDevicePath);
        wcscpy_s(infoEx.ex.dstMonitorName, targetName.monitorFriendlyDeviceName);

        isDefault = wcsstr(infoEx.ex.dstMonitorPath, L"Default_Monitor") != nullptr; //不插显示器开机时系统的默认路径
        bool hasName = wcslen(infoEx.ex.dstMonitorName); //系统默认监视器就没有名称
        bool hasPath = wcslen(infoEx.ex.dstMonitorPath);

        //TODO: 系统会保留以往插过的显示器配置,但如果当前没有插入则不会有监视器的设备路径
        //TODO: 在不插任何显示器是系统有默认的输出监视器,且没有名称,在变为默认监视器时好像还会保留最后一次配置正确的 dstOutputTechnology
        //所以不能单纯的用有没有名称来判断有效性,此处同时使用监视器设备路径来确定,只有有路径则说明设备在硬件层面上已连接
        if (hasPath)
        {
            DISPLAYCONFIG_SOURCE_DEVICE_NAME deviceName{};
            deviceName.header.size = sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME);
            deviceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
            deviceName.header.adapterId = srcAdapterId;
            deviceName.header.id = srcId;
            if (DisplayConfigGetDeviceInfo(&deviceName.header) == ERROR_SUCCESS)
                wcscpy_s(infoEx.ex.srcDevName, deviceName.viewGdiDeviceName);

            DISPLAYCONFIG_ADAPTER_NAME adapterName{};
            adapterName.header.size = sizeof(DISPLAYCONFIG_ADAPTER_NAME);
            adapterName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADAPTER_NAME;
            adapterName.header.adapterId = srcAdapterId;
            if (DisplayConfigGetDeviceInfo(&adapterName.header) == ERROR_SUCCESS)
                wcscpy_s(infoEx.ex.srcAdapterPath, adapterName.adapterDevicePath) == 0;

            adapterName.header.adapterId = dstAdapterId;
            if (DisplayConfigGetDeviceInfo(&adapterName.header) == ERROR_SUCCESS)
                wcscpy_s(infoEx.ex.dstAdapterPath, adapterName.adapterDevicePath) == 0;

            DISPLAYCONFIG_TARGET_PREFERRED_MODE preferredMode{};
            preferredMode.header.size = sizeof(DISPLAYCONFIG_TARGET_PREFERRED_MODE);
            preferredMode.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE;
            preferredMode.header.adapterId = dstAdapterId;
            preferredMode.header.id = dstId;
            if (DisplayConfigGetDeviceInfo(&preferredMode.header) == ERROR_SUCCESS)
            {
                infoEx.ex.dstWidth = preferredMode.width;
                infoEx.ex.dstHeight = preferredMode.height;
                infoEx.ex.dstMode = preferredMode.targetMode;
            }

            DEVMODE devMode{};
            devMode.dmSize = sizeof(DEVMODE);
            if (EnumDisplaySettingsEx(infoEx.ex.srcDevName, ENUM_REGISTRY_SETTINGS, &devMode, EDS_RAWMODE)) //直接从注册设置中取得像素位
                infoEx.ex.dstPixelFormat =
                devMode.dmBitsPerPel == 8
                ? DISPLAYCONFIG_PIXELFORMAT_8BPP
                : devMode.dmBitsPerPel == 16
                ? DISPLAYCONFIG_PIXELFORMAT_16BPP
                : devMode.dmBitsPerPel == 24
                ? DISPLAYCONFIG_PIXELFORMAT_24BPP
                : devMode.dmBitsPerPel == 32
                ? DISPLAYCONFIG_PIXELFORMAT_32BPP
                : DISPLAYCONFIG_PIXELFORMAT_NONGDI;

            if (setInfo.modePtr && setInfo.modeCount)
                for (int srcModeIdx = 0; srcModeIdx < setInfo.modeCount; srcModeIdx++)
                {
                    const DISPLAYCONFIG_MODE_INFO &modeRef = setInfo.modePtr.get()[srcModeIdx];
                    if (modeRef.id == infoEx.path.sourceInfo.id)
                        infoEx.ex.srcMode = setInfo.modePtr.get()[srcModeIdx].sourceMode;
                }

            return true;
        }
    }

    return false;
}

  当前期扩展信息获取可用以后,现在我们准备一下 Debug 环境,实现对有效扩展信息的打印,帮助后期分析。

#define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__)
#define WLOG(fmt, ...) wprintf(fmt, ##__VA_ARGS__)

#define MODE_TYPE(type) \
		type == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE \
		? L"source" \
		: type == DISPLAYCONFIG_MODE_INFO_TYPE_TARGET \
		? L"target" \
		: L"other"

#define MODE_POSITION_X(type, modeRef) \
		type == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE \
		? modeRef.sourceMode.position.x \
		: -1

#define MODE_POSITION_Y(type, modeRef) \
		type == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE \
		? modeRef.sourceMode.position.y \
		: -1

#define MODE_INFO_FMT L"MODE==> \t idx:%d \t id:0x%08x \t adpId:0x%08x \t type:%s \t x:%4d  y:%4d \n"

#define SHOW_MODE_INFO(i, modeRef) WLOG(MODE_INFO_FMT \
		, i \
		, modeRef.id \
		, modeRef.adapterId.LowPart \
		, MODE_TYPE(modeRef.infoType) \
		, MODE_POSITION_X(modeRef.infoType, modeRef) \
		, MODE_POSITION_Y(modeRef.infoType, modeRef) \
		)

#define PATH_TECH(tech) \
		tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HD15 \
		? L"VGA" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SVIDEO \
		? L"SVIDEO" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPOSITE_VIDEO \
		? L"COMPOSITE_VIDEO" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPONENT_VIDEO \
		? L"COMPONENT_VIDEO" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DVI \
		? L"DVI" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI \
		? L"HDMI" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_LVDS \
		? L"LVDS" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_D_JPN \
		? L"D_JPN" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDI \
		? L"SDI" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL \
		? L"DISPLAYPORT_EXTERNAL" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED \
		? L"DISPLAYPORT_EMBEDDED" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EXTERNAL \
		? L"UDI_EXTERNAL" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED \
		? L"UDI_EMBEDDED" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDTVDONGLE \
		? L"SDTVDONGLE" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_MIRACAST \
		? L"MIRACAST" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL \
		? L"INTERNAL" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_FORCE_UINT32 \
		? L"FORCE_UINT32" \
		: tech == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER \
		? L"OTHER" \
		: L"Error"

#define PATH_INFO_MFT L"PATH==> \t idx:%d \t active:%d \t width:%d \t height:%d \n" \
		L"\t\t srcDevName:%s \t moitName:%s \t signal:%s \n" \
		L"\t\t srcId:0x%08x \t srcAdpId:0x%08x \t srcModeIdx:%d \n" \
		L"\t\t dstId:0x%08x \t dstAdpId:0x%08x \t dstModeIdx:%d \n" \
		L"moitDevPath:%s \r\n\n"

#define SHOW_PATH_INFO(i, infoEx) WLOG(PATH_INFO_MFT \
		, i \
		, infoEx.path.flags & DISPLAYCONFIG_PATH_ACTIVE \
		, infoEx.ex.dstWidth \
		, infoEx.ex.dstHeight \
		, infoEx.ex.srcDevName \
		, infoEx.ex.dstMonitorName \
		, PATH_TECH(infoEx.path.targetInfo.outputTechnology) \
		, infoEx.path.sourceInfo.id \
		, infoEx.path.sourceInfo.adapterId.LowPart \
		, infoEx.path.sourceInfo.modeInfoIdx \
		, infoEx.path.targetInfo.id \
		, infoEx.path.targetInfo.adapterId.LowPart \
		, infoEx.path.targetInfo.modeInfoIdx \
		, infoEx.ex.dstMonitorPath \
		)

void WDDM::ShowSettingInfo(DISPLAYCONFIG_SETTING_INFO& setInfo
    , std::vector<DISPLAYCONFIG_PATH_INFO_EX>& infoExVec)
{
    if (setInfo.modePtr && setInfo.modeCount)
        for (size_t i = 0; i < setInfo.modeCount; i++)
        {
            const DISPLAYCONFIG_MODE_INFO& modeRef = setInfo.modePtr.get()[i];
            SHOW_MODE_INFO(i, modeRef);
        }

    LOG("\n");

    int validCount = infoExVec.size();
    if (validCount > 0)
        for (size_t i = 0; i < validCount; i++)
        {
            const DISPLAYCONFIG_PATH_INFO_EX& infoEx = infoExVec[i];
            SHOW_PATH_INFO(i, infoEx);
        }
}

  此时扩展数据的获取和打印环境以准备完成,对此我们将这些扩展信息打印出来。

DISPLAYCONFIG_SETTING_INFO setInfo{};
if (WDDM::GetPathModeInfo(setInfo, true))
{
    std::vector<DISPLAYCONFIG_PATH_INFO_EX> infoExVec;
    if (setInfo.pathPtr && setInfo.pathCount)
        for (int i = 0; i < setInfo.pathCount; i++)
        {
            const DISPLAYCONFIG_PATH_INFO& infoRef = setInfo.pathPtr.get()[i];
            DISPLAYCONFIG_PATH_INFO_EX infoEx{ infoRef };

            bool isDefault = false;
            if (WDDM::FillInfoEx(setInfo, infoEx, isDefault))
                infoExVec.push_back(infoEx);
        }

    WDDM::ShowSettingInfo(setInfo, infoExVec);
}

  到此时我们可以得到如下的调试信息,将帮助我理解 PATHMODE 的相互关系,需要注意的是在 FillInfoEx 阶段已经过滤了无效的项目。

MODE==>          idx:0   id:0x03060300   adpId:0x0000b808        type:target     x:  -1  y:  -1
MODE==>          idx:1   id:0x00000000   adpId:0x0000b808        type:source     x:1920  y:   0
MODE==>          idx:2   id:0x01010100   adpId:0x0000b808        type:target     x:  -1  y:  -1
MODE==>          idx:3   id:0x00000001   adpId:0x0000b808        type:source     x:   0  y:   0

PATH==>          idx:0   active:1        width:1920      height:1080
srcDevName:\\.\DISPLAY1         moitName:3DTV   signal:HDMI
srcId:0x00000000        srcAdpId:0x0000b808     srcModeIdx:1
dstId:0x03060300        dstAdpId:0x0000b808     dstModeIdx:0
moitDevPath:\\?\DISPLAY#TCL0000#4&35aefc14&0&UID50725632#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

PATH==>          idx:1   active:1        width:1920      height:1080
srcDevName:\\.\DISPLAY2         moitName:H220   signal:VGA
srcId:0x00000001        srcAdpId:0x0000b808     srcModeIdx:3
dstId:0x01010100        dstAdpId:0x0000b808     dstModeIdx:2
moitDevPath:\\?\DISPLAY#HKC21A6#4&35aefc14&0&UID16843008#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

PATH==>          idx:2   active:0        width:1920      height:1080
srcDevName:\\.\DISPLAY1         moitName:H220   signal:VGA
srcId:0x00000000        srcAdpId:0x0000b808     srcModeIdx:1
dstId:0x01010100        dstAdpId:0x0000b808     dstModeIdx:2
moitDevPath:\\?\DISPLAY#HKC21A6#4&35aefc14&0&UID16843008#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

PATH==>          idx:3   active:0        width:1920      height:1080
srcDevName:\\.\DISPLAY3         moitName:H220   signal:VGA
srcId:0x00000002        srcAdpId:0x0000b808     srcModeIdx:-1
dstId:0x01010100        dstAdpId:0x0000b808     dstModeIdx:2
moitDevPath:\\?\DISPLAY#HKC21A6#4&35aefc14&0&UID16843008#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

PATH==>          idx:4   active:0        width:1920      height:1080
srcDevName:\\.\DISPLAY2         moitName:3DTV   signal:HDMI
srcId:0x00000001        srcAdpId:0x0000b808     srcModeIdx:3
dstId:0x03060300        dstAdpId:0x0000b808     dstModeIdx:0
moitDevPath:\\?\DISPLAY#TCL0000#4&35aefc14&0&UID50725632#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

PATH==>          idx:5   active:0        width:1920      height:1080
srcDevName:\\.\DISPLAY3         moitName:3DTV   signal:HDMI
srcId:0x00000002        srcAdpId:0x0000b808     srcModeIdx:-1
dstId:0x03060300        dstAdpId:0x0000b808     dstModeIdx:0
moitDevPath:\\?\DISPLAY#TCL0000#4&35aefc14&0&UID50725632#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

  基于以上的调试信息我们可以知道下面这些有用的规律信息:

  • 在同一适配器(dstAdpId)上的不同监视器(moitDevPath)可被安排到不同编号(srcDevName)上。如果将过滤掉的数据全部打印出来,则会发现所有的 PATH 其实是对适配器、编号、监视器这三者的穷举排列。

  • PATH 的打印中 srcIdsrcModeIdxdstIddstModeIdx 均对于与 MODE打印中的 ididx,其中 idx (数组索引的简写)不存在时用 “-1” 代替。同时该条路径是否处于激活由 DISPLAYCONFIG_PATH_INFO 中的 flags 是否设置 DISPLAYCONFIG_PATH_ACTIVE 确定。在某些工控类主板中,可能出现打印的 signal 不是你实际硬件的类型,比如打印为 DISPLAYPORT_EXTERNAL 但实际硬件接口却是 VGA,出现这种情况一般是主板使用显示信号转换芯片。

  • MODE 的打印中 target 在前 source 在后。

  到此时基本关系已经明确,将一个监视器 moitDevPath 绑定在编号 srcDevName 上。设置需要的分辨率到 srcMode 并将其连接到 dstMode,由此构建一条完整的 PATH ,两者之间的关系可做下图说明:

flowchart LR subgraph MODE array dstMode1[idx:0 target mode1] srcMode1[idx:1 source mode1] dstMode2[idx:2 target mode2] srcMode2[idx:3 source mode2] end subgraph PATH array path1[idx:0 path1] path2[idx:1 path2] end dstMode1 -->|dstModeIdx| path1 srcMode1 -->|srcModeIdx| path1 dstMode2 -->|dstModeIdx| path2 srcMode2 -->|srcModeIdx| path2

  为此要自定义显示模式则只需要构建一个满足上方规则的 MODE 数组和 PATH 数组。将其传递给传递给 SetDisplayConfig 执行设置,不要忘了给 PATHflags 设定 DISPLAYCONFIG_PATH_ACTIVE 激活,该接口函数如果需要立即生效则需要注意使用以下参数搭配:

bool SetSettingInfo(const DISPLAYCONFIG_SETTING_INFO &setInfo)
{
	// SetDisplayConfig 要求程序是桌面程序,拥有访问控制台会话或桌面否者将其返回 ERROR_ACCESS_DENIED
	return SetDisplayConfig(setInfo.pathCount
		, setInfo.pathPtr.get()
		, setInfo.modeCount
		, setInfo.modePtr.get()
		, SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_APPLY | SDC_SAVE_TO_DATABASE | SDC_ALLOW_CHANGES | SDC_FORCE_MODE_ENUMERATION
		) == ERROR_SUCCESS;
}

2、 如何定制显示模式

  定制显示模式主要在于对 DISPLAYCONFIG_MODE_INFO中的sourceMode的合理设置。

2.1 单主屏幕显示

  实现单主屏显示效果,则我们值需要构建一条 PATH 即可。由于单屏显示不需要设置如扩展或者复制模式,则我应该将其 PATH 对应的DISPLAYCONFIG_MODE_INFO中的 sourceMode 下的 position 重置为“0,0”。

2.2 双屏显示如何设置

  在多屏幕模式下主要有复制和扩展这两种模式,这两种模式设置方法一样,单使用的配置参数不一致,两种模式都有自己的强制要求。

2.2.1 复制模式

  要构建一个复制模式则可先分析复制模式的显示特点,两个屏幕会显示一样的分辨率、显示位置。在一个外接两块屏幕的系统中设置复制模式,则在构建这两条 PATH 时,每一条 PATH 对应的DISPLAYCONFIG_MODE_INFO中的 sourceMode 下的 position 一定要都为“0,0”。其主要伪代码操作如下:

srcModeInfo1.sourceMode.width = 1366;
srcModeInfo1.sourceMode.height = 768;
srcModeInfo1.sourceMode.position.x = 0;
srcModeInfo1.sourceMode.position.y = 0;
...
srcModeInfo2.sourceMode.width = 1366;
srcModeInfo2.sourceMode.height = 768;
srcModeInfo2.sourceMode.position.x = 0;
srcModeInfo2.sourceMode.position.y = 0;

2.2.1 扩展模式

  扩展模式相对自由度更高,每个显示器可以自定义的自己的分辨率和显示位置。但扩展模式有一个硬性要求就是两条路径形成的矩形的之间不能有缝隙,两个矩形至少有一个共同的边,否则将会设置无效。该规则基于Window系统分辨以屏幕左上角为原点,以主屏幕左上角 0,0 为参考。

flowchart LR L[-1366,0] --- M[0,0] --- R[1920,0]

  基于横向的排列规则,将屏幕2放置在主屏幕左側则可以有如下伪代码:

srcModeInfo1.sourceMode.width = 1920;
srcModeInfo1.sourceMode.height = 1080;
srcModeInfo1.sourceMode.position.x = 0;
srcModeInfo1.sourceMode.position.y = 0;
...
srcModeInfo2.sourceMode.width = 1366;
srcModeInfo2.sourceMode.height = 768;
srcModeInfo2.sourceMode.position.x = -1366;
srcModeInfo2.sourceMode.position.y = 0;
flowchart TB A[0,-768] --- C[0,0] --- B[0,1080]

  基于竖向的排列规则,将屏幕2放置在主屏幕上側则可以有如下伪代码:

srcModeInfo1.sourceMode.width = 1920;
srcModeInfo1.sourceMode.height = 1080;
srcModeInfo1.sourceMode.position.x = 0;
srcModeInfo1.sourceMode.position.y = 0;
...
srcModeInfo2.sourceMode.width = 1366;
srcModeInfo2.sourceMode.height = 768;
srcModeInfo2.sourceMode.position.x = 0;
srcModeInfo2.sourceMode.position.y = -768;

  有一个特例是斜角45°排列,则两矩形的两个顶点相靠,将屏幕2放置在主屏幕左上斜角45°位置则可以有如下伪代码:

srcModeInfo1.sourceMode.width = 1920;
srcModeInfo1.sourceMode.height = 1080;
srcModeInfo1.sourceMode.position.x = 0;
srcModeInfo1.sourceMode.position.y = 0;
...
srcModeInfo2.sourceMode.width = 1366;
srcModeInfo2.sourceMode.height = 768;
srcModeInfo2.sourceMode.position.x = -1366;
srcModeInfo2.sourceMode.position.y = -768;