Windows Multi-monitor: How can I determine if a target is physically connected to a source when the target is available but not active?
Asked Answered
E

1

8

I want to enable a particular disabled monitor, based on info originating from DISPLAYCONFIG_TARGET_DEVICE_NAME and/or DISPLAYCONFIG_PATH_TARGET_INFO. To actually enable this monitor, all I need to do is to successfully map this to the matching devicename name to enable, e.g. \\.\DISPLAY1. But I cannot find any general way to make this determination without preexisting special knowledge. If only I could relate it to the actually relevant matching DISPLAYCONFIG_PATH_SOURCE_INFO.

QueryDisplayConfig is returning every possible combination of source and target on my machine, even pairing monitors with sources they are not actually connected to. I have 4 ports and 3 monitors, so I get 12 combinations that have targetAvailable in the target, because it repeats each target with relevant and irrelevant sources. Because I get source+target combinations that are not real, I can't determine which source is really physically connected to which target, unless the source+target pair is already active, e.g. DISPLAYCONFIG_PATH_INFO::flags has DISPLAYCONFIG_PATH_ACTIVE. Then I can easily tell what's going on.

Basically, as long as the target is in use / attached to the desktop, there is no problem whatsoever; there are numerous ways to associate which source it is connected with. But in this scenario, the target is disabled, but connected (meaning in the control panel, the monitor is available but excluded from the multi-monitor setup). The API shows the disabled device without issue, but I cannot determine what port it is connected to or which devicename to enable. Because the monitor is disabled, EnumDisplayMonitors is useless.

Obviously EnumDisplayDevices will give me the IDevNum and deviceName of every possible thing to enable, but nothing in this API will connect me to a DISPLAYCONFIG_TARGET_DEVICE_NAME, since I am unable to associate sources with their connected targets as described above. So my only choice appears to be to blindly enable a monitor, with no way to ensure I'm enabling the correct one which matches my target structs.

Does anyone know these APIs well enough to provide assistance? My hunch is that I will need to leverage something beyond the API's I've been trying to use, since I've gone over all of their potential outputs in the debugger with a fine tooth comb, but I could be missing something. Maybe there is something stored in the registry I can use to connect the dots? I would be willing to consider using an undocumented api or structure if necessary.

Thanks

Esau answered 14/3, 2014 at 8:23 Comment(2)
You may want to consider replacing one of your tags (I recommend multiple-monitors) with a language tag so any code offered is directly relevant to you.Roentgenograph
You might want to look at the SetupDiXxx device information APIs. They will at least allow you to determine which monitor is connected to which graphics card, though I have no idea how to distinguish between different ports on the same card.Nupercaine
E
15

I figured this out, and hopefully this answer will help someone. Ironically, in my question, I kind of guessed what the answer would be, without realizing it! I had said

my only choice appears to be to blindly enable a monitor.

Which turns out to not be that bad at all, because SetDisplayConfig has a flag called SDC_VALIDATE, which merely tests if the config is okay and doesn't impact the user if I call it. So to figure out which source is connected to which target, all I have to do is try to enable each source+target pair that contains my target until one works. The real source+target pair will succeed, whereas the fake ones return ERROR_GEN_FAILURE. It's a fairly obtuse and lengthy method, and to my knowledge this scenario is totally undocumented, but it does make some intuitive sense in a way: simply identify the source+target pair that is possible to enable and that's the source you want.

Here is some sample code for it:

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.

That is the answer to this question, but I decided to post some other related discoveries too. So what all can you do once you know the source for the target you're interested in?

One thing you can do is find the Gdi Device Name for the source, e.g. \\.\DISPLAY1, using 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

Note that DisplayConfigGetDeviceInfo can get you the friendly name for a target too. If you scanned all the targets for one matching your attached display, e.g. "PanasonicTV0" or "SyncMaster" or whatever, you could use that target as the input to the above method. This gives you enough to string together code for the entire end-to-end implementation for EnableDisplay("SyncMaster") or somesuch thing.

Since you can now get the GdiDeviceName, you could ChangeDisplaySettingsEx to make it the primary monitor too. One of the secrets to applying CDS_SET_PRIMARY correctly is that the primary monitor must have DM_POSITION of 0,0 and you have to update ALL monitors to be adjacent to the new corrected position. I have sample code for that too:

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;
}
Esau answered 16/3, 2014 at 20:52 Comment(2)
For anyone copy & pasting the code, there is a memory leak in the failure case around QueryDisplayConfig. Each loop will allocate memory for new arrays. As you're using new I'm going to assume C++ usage, so just use a std::vector and call v.resize() on it each loop (use v.data() to access the mutable buffer).Silber
I'd love to see your input on https://mcmap.net/q/440875/-how-can-i-change-windows-10-display-scaling-programmatically-using-cAlcazar

© 2022 - 2024 — McMap. All rights reserved.