Windows 多显示器:当目标可用但不活动时,如何确定目标是不是物理连接到源?

Posted

技术标签:

【中文标题】Windows 多显示器:当目标可用但不活动时,如何确定目标是不是物理连接到源?【英文标题】:Windows Multi-monitor: How can I determine if a target is physically connected to a source when the target is available but not active?Windows 多显示器:当目标可用但不活动时,如何确定目标是否物理连接到源? 【发布时间】:2014-04-19 10:14:40 【问题描述】:

我想根据来自DISPLAYCONFIG_TARGET_DEVICE_NAME 和/或DISPLAYCONFIG_PATH_TARGET_INFO 的信息启用特定的禁用监视器。要真正启用此监视器,我需要做的就是成功地将其映射到要启用的 ma​​tching 设备名称,例如\\.\DISPLAY1。但是如果没有预先存在的特殊知识,我找不到任何通用的方法来做出这个决定。要是我能把它与匹配DISPLAYCONFIG_PATH_SOURCE_INFO实际相关联系起来就好了。

QueryDisplayConfig 在我的机器上返回所有可能的源和目标组合,甚至将监视器与它们实际未连接的源配对。我有 4 个端口和 3 个监视器,所以我得到了 12 个在目标中包含 targetAvailable 的组合,因为它使用相关和不相关的源重复每个目标。因为我得到了不真实的源+目标组合,所以我无法确定哪个源真正物理连接到哪个目标,除非源+目标对是已经激活,例如DISPLAYCONFIG_PATH_INFO::flagsDISPLAYCONFIG_PATH_ACTIVE。然后我可以很容易地知道发生了什么。

基本上,只要目标正在使用/连接到桌面,就没有任何问题;有很多方法可以关联它与哪个源相关联。但在这种情况下,目标被禁用,但已连接(意味着在控制面板中,显示器可用,但从多显示器设置中排除)。 API 显示禁用的设备没有问题,但我无法确定 它连接到哪个端口或启用哪个设备名称。因为监视器被禁用,EnumDisplayMonitors 没用。

显然EnumDisplayDevices 会给我IDevNumdeviceName 的所有可能启用的东西,但是这个API 中没有任何东西可以将我连接到DISPLAYCONFIG_TARGET_DEVICE_NAME,因为我无法将源与其连接的目标相关联如上所述。所以我唯一的选择似乎是盲目地启用监视器,无法确保我启用了与我的目标结构匹配的正确监视器。

有没有人足够了解这些 API 可以提供帮助?我的预感是,我需要利用超出我一直尝试使用的 API 的东西,因为我已经在调试器中用细齿梳检查了它们的所有潜在输出,但我可能会遗漏一些东西。也许注册表中存储了一些东西可以用来连接这些点?如有必要,我愿意考虑使用未记录的 api 或结构。

谢谢

【问题讨论】:

您可能需要考虑用语言标签替换您的标签之一(我推荐multiple-monitors),以便提供的任何代码都与您直接相关。 您可能需要查看 SetupDiXxx 设备信息 API。它们至少可以让您确定哪个显示器连接到哪个显卡,尽管我不知道如何区分同一张卡上的不同端口。 【参考方案1】:

我想通了,希望这个答案对某人有所帮助。具有讽刺意味的是,在我的问题中,我有点猜到答案会是什么,却没有意识到!我说过

我唯一的选择似乎是盲目地启用监视器。

结果并没有那么糟糕,因为 SetDisplayConfig 有一个名为 SDC_VALIDATE 的标志,它仅测试配置是否正常,如果我调用它,它不会影响用户。所以要弄清楚哪个源连接到哪个目标,我所要做的就是尝试启用包含我的目标的每个源+目标对,直到一个工作。 真正的源+目标对将成功,而假的则返回ERROR_GEN_FAILURE。这是一种相当迟钝且冗长的方法,据我所知,这种情况完全没有文档记录,但它在某种程度上确实具有某种直观意义:只需确定可以启用的源+目标对,这就是您想要的源。

下面是一些示例代码:

LUID& targetAdapter; // the LUID of the target we want to find the source for
ULONG targetId;  // the id of the target we want to find the source for

DISPLAYCONFIG_PATH_SOURCE_INFO* pSource = NULL; // will contain the answer

DISPLAYCONFIG_PATH_INFO* pPathInfoArray = NULL;
DISPLAYCONFIG_MODE_INFO* pModeInfoArray = NULL;
UINT32 numPathArrayElements;
UINT32 numModeInfoArrayElements;

// First, grab the system's current configuration
for (UINT32 tryBufferSize = 32;; tryBufferSize <<= 1)

    pPathInfoArray = new DISPLAYCONFIG_PATH_INFO[tryBufferSize];
    pModeInfoArray = new DISPLAYCONFIG_MODE_INFO[tryBufferSize];
    numPathArrayElements = numModeInfoArrayElements = tryBufferSize;

    ULONG rc = QueryDisplayConfig(
        QDC_ALL_PATHS,
        &numPathArrayElements,
        pPathInfoArray,
        &numModeInfoArrayElements,
        pModeInfoArray,
        NULL);

    if (rc == ERROR_SUCCESS)
        break;

    if (rc != ERROR_INSUFFICIENT_BUFFER || tryBufferSize > 1024)
        return; // failure


// Narrow down the source that's truly connected to our target.
// Try "test" enabling one <source>+<ourtarget> pair at a time until we have the right one
for (int tryEnable = 0;; ++tryEnable)

    DISPLAYCONFIG_PATH_INFO* pCurrentPath = NULL;
    for (UINT32 i = 0, j = 0; i < numPathArrayElements; ++i)
    
        if (pPathInfoArray[i].targetInfo.targetAvailable &&
            !memcmp(&pPathInfoArray[i].targetInfo.adapterId, &targetAdapter, sizeof (LUID)) &&
            pPathInfoArray[i].targetInfo.id == targetId)
        
            pPathInfoArray[i].targetInfo.statusFlags |= DISPLAYCONFIG_TARGET_IN_USE;

            if (j++ == tryEnable)
            
                pCurrentPath = &pPathInfoArray[i];

                if (pCurrentPath->flags & DISPLAYCONFIG_PATH_ACTIVE)
                
                    // trivial early out... user already had this enabled, therefore we know this is the right source.
                    pSource = &pCurrentPath->sourceInfo;
                    break; 
                

                // try to activate this particular source
                pCurrentPath->flags |= DISPLAYCONFIG_PATH_ACTIVE;
                pCurrentPath->sourceInfo.statusFlags |= DISPLAYCONFIG_SOURCE_IN_USE;
            
        
    

    if (!pCurrentPath)
        return; // failure. tried everything, apparently no source is connected to our target

    LONG rc = SetDisplayConfig(
        numPathArrayElements,
        pPathInfoArray,
        numModeInfoArrayElements,
        pModeInfoArray,
        SDC_VALIDATE | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES);

    if (rc != ERROR_SUCCESS)
    
        // it didn't work, undo trying to activate this source
        pCurrentPath->flags &= ~DISPLAYCONFIG_PATH_ACTIVE;
        pCurrentPath->sourceInfo.statusFlags &= DISPLAYCONFIG_SOURCE_IN_USE;
    
    else
    
        pSource = &pCurrentPath->sourceInfo;
        break; // success!
    

//Note: pSource is pointing to the source relevant to the relevant source now! 
//You just need to copy off whatever you need.

这就是这个问题的答案,但我决定也发布一些其他相关的发现。那么,一旦你知道了你感兴趣的目标的来源,你能做些什么呢?

您可以做的一件事是找到源的 Gdi 设备名称,例如\\.\DISPLAY1,使用DisplayConfigGetDeviceInfo

DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceInfo;
ZeroMemory(&sourceInfo, sizeof(sourceInfo));
sourceInfo.header.size = sizeof(queryInfo.source);
sourceInfo.header.adapterId = pSource->adapterId;
sourceInfo.header.id = pSource->id;
sourceInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
ULONG rc = DisplayConfigGetDeviceInfo(&sourceInfo.header);

if (rc == ERROR_SUCCESS)
    cout << queryInfo.source.viewGdiDeviceName; // e.g. \\.\DISPLAY1

请注意,DisplayConfigGetDeviceInfo 也可以为您提供目标 的友好名称。如果您扫描了所有目标以找到与您连接的显示器匹配的目标,例如“PanasonicTV0”或“SyncMaster”或其他,您可以使用该目标作为上述方法的输入。这使您可以将EnableDisplay("SyncMaster") 或类似事物的整个端到端实现的代码串在一起。

既然您现在可以获得GdiDeviceName,您也可以将ChangeDisplaySettingsEx 设为主显示器。正确应用 CDS_SET_PRIMARY 的秘诀之一是主监视器的 DM_POSITION 必须为 0,0,并且您必须更新所有监视器以与新的更正位置相邻。我也有示例代码:

HRESULT ChangePrimaryMonitor(wstring gdiDeviceName)

    HRESULT hr;
    wstring lastPrimaryDisplay = L"";
    bool shouldRefresh = false;

    DEVMODE newPrimaryDeviceMode;
    newPrimaryDeviceMode.dmSize = sizeof(newPrimaryDeviceMode);
    if (!EnumDisplaySettings(gdiDeviceName.c_str(), ENUM_CURRENT_SETTINGS, &newPrimaryDeviceMode))
    
        hr = E_FAIL;
        goto Out;
    

    for (int i = 0;; ++i)
    
        ULONG flags = CDS_UPDATEREGISTRY | CDS_NORESET;
        DISPLAY_DEVICE device;
        device.cb = sizeof(device);
        if (!EnumDisplayDevices(NULL, i, &device, EDD_GET_DEVICE_INTERFACE_NAME))
            break;

        if ((device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) == 0)
            continue;

        if (!wcscmp(device.DeviceName, gdiDeviceName.c_str()))
            flags |= CDS_SET_PRIMARY;

        DEVMODE deviceMode;
        newPrimaryDeviceMode.dmSize = sizeof(deviceMode);
        if (!EnumDisplaySettings(device.DeviceName, ENUM_CURRENT_SETTINGS, &deviceMode))
        
            hr = E_FAIL;
            goto Out;
        

        deviceMode.dmPosition.x -= newPrimaryDeviceMode.dmPosition.x;
        deviceMode.dmPosition.y -= newPrimaryDeviceMode.dmPosition.y;
        deviceMode.dmFields |= DM_POSITION;

        LONG rc = ChangeDisplaySettingsEx(device.DeviceName, &deviceMode, NULL,
            flags, NULL);

        if (rc != DISP_CHANGE_SUCCESSFUL) 
            hr = E_FAIL;
            goto Out;
        

        shouldRefresh = true;
    

    hr = S_OK;

    Out:

    if (shouldRefresh)
        ChangeDisplaySettingsEx(NULL, NULL, NULL, CDS_RESET, NULL);

    return hr;

【讨论】:

感谢您分享您的发现。 +1 对于任何复制和粘贴代码的人,在QueryDisplayConfig 附近的失败案例中存在内存泄漏。每个循环都会为新数组分配内存。当您使用 new 时,我将假设使用 C++,因此只需使用 std::vector 并在每个循环上调用 v.resize()(使用 v.data() 访问可变缓冲区)。 我很想在***.com/a/57397039/1208190看到您的意见

以上是关于Windows 多显示器:当目标可用但不活动时,如何确定目标是不是物理连接到源?的主要内容,如果未能解决你的问题,请参考以下文章

Android TV - Exoplayer 控件显示在模拟器上但不显示在电视上

Google 日历活动已插入但不存在

如何为 Windows 7 编写进度条以在任务栏上进行自我更新?

每当从 sqlite 数据库中删除记录时,将活动导航到上一个并显示数据库中可用的记录

如何处理ios目标c中的解析通知?

交换JPanel,第二个JPanel不显示