6

I'm developing an application which will display information derived from the EDID blocks (monitor model, ID, S/N, etc.) on a dialog on the corresponding monitor.

This code works for finding the EDID information for displays. It extracts the EDID information by enumerating the DISPLAY keys under HKLM\SYSTEM\CurrentControlSet\Enum\DISPLAY\[Monitor]\[PnPID]\Device Parameters\EDID.

Update: The above code is relying on "side effects" of PnP use of the registry. I am now using the SetupAPI to enumerate monitors, which correctly handles monitors being attached/removed (unlike the code from the link above.)

I am trying to correlate each Screen in Windows.Forms.Screen.AllScreens[] (\\.\DISPLAY1, \\.\DISPLAY2, etc.) with the entries returned from the above registry inspection.

Note: In the code block below, DisplayDetails.GetMonitorDetails() has now been replaced with more robust registry enumeration code using the SetupAPI, but the data returned is the same.

e.g.

private void Form1_Load(object sender, EventArgs e)
{
    Console.WriteLine("Polling displays on {0}:", System.Environment.MachineName);
    int i = 0;
    foreach ( DisplayDetails dd in DisplayDetails.GetMonitorDetails())
    {
        Console.WriteLine( "Info: Model: {0}, MonitorID: {1}, PnPID: {2}, Serial#:{3}", dd.Model, dd.MonitorID, dd.PnPID, dd.SerialNumber );
        Console.WriteLine( "Does this correlate to Screen: {0}?", Screen.AllScreens[i++].DeviceName );
    }
}

Output:

Info: Model: DELL P2411H, MonitorID: DELA06E, PnPID: 5&2e2fefea&0&UID1078018, Serial#:F8NDP0C...PU

Does this correlate to Screen: \\.\DISPLAY1?

Info: Model: DELL P2411H, MonitorID: DELA06E, PnPID: 5&2e2fefea&0&UID1078019, Serial#:F8NDP0C...AU

Does this correlate to Screen: \\.\DISPLAY2?


Answer: NO

In testing, I've found these don't reliably correlate (I have a system in which the first display enumerated is \\.\DISPLAY2).

My Question: Is there a way to reliably get the EDID information for a given Forms.Screen? I can get the EDID block, but have found no path to correlate this up to the UI top-level Form. Prompting the user is undesirable, as in my use case the two (or more) monitors will likely be the same model and resolution, and only differ by a few digits in the S/N.

I've looked for paths following the Forms.Screen API, Win32 EnumDisplay, other registry GUIDs (PnP and driver-related), but haven't found any promising paths.

I have also investigated the WMI Win32_DesktopMonitor API (Windows 7), however it doesn't appear to have any more information that would help me correlate it to the Windows.Forms.Screen.AllScreens[] entries.

I suspect if there is a way to do this, it's through the SetupAPI, however I haven't found it yet.

holtavolt
  • 4,307
  • 1
  • 24
  • 40
  • 3
    You are not supposed to use the registry key directly. Proper way is through SetupDiGetClassDevices(GUID_DEVINTERFACE_MONITOR) + SetupDiEnumDeviceInterfaces + SetupDiOpenDevRegKey. Some odds that this can respond to PnP events better, no idea if it does. – Hans Passant Apr 20 '12 at 01:47
  • A believe you're suggesting that using the SetupAPI might avoid the "false positive" problem on a disconnect, and that I continue to rely on the order of enumeration matching the entries in Forms.Screen.AllScreens[]? That's worth a try. – holtavolt Apr 20 '12 at 04:47
  • The good news: using the SetupAPI to enumerate the devices does remove the "false positive", and reliably returns only the currently attached displays (unlike the DisplayInfoWMIProvider code I had been using). The bad news: I have a test configuration that fails to match the correct display (Forms.Screen.AllScreens[] doesn't match enum order). I'm still searching for a solution. – holtavolt Apr 24 '12 at 12:22
  • NOTE: Bounty is now looking for answer to part a) __...solution that explicitly binds the two...__ Sorry - I can't update the bounty text (http://meta.stackexchange.com/questions/16065/how-does-the-bounty-system-work) – holtavolt Apr 24 '12 at 12:49
  • Get the device name out of SetupDiGetDeviceInterfaceDetail(). Don't assume an order. – Hans Passant Apr 24 '12 at 13:02
  • Avoiding assuming an order is my goal. SetupDiGetDeviceInterfaceDetail() returns info like __DISPLAY\DELA06E\5&2E2FEFEA&0&UID1078018__ which doesn't get me any closer to mapping to a top-level Forms.Screen.AllScreens[] entry. – holtavolt Apr 24 '12 at 13:27
  • The DIDD doesn't resolve it, either: __DevicePath: \\?\display#dela06e#5&2e2fefea&0&uid1078018#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}__. The only out parms for SetupDiGetDeviceInterfaceDetail() are for the DIDD and DID (I used CM_Get_Device_ID() on the DID for the comment above, but I see notes that the CM_* API should now be avoided.) – holtavolt Apr 24 '12 at 13:47
  • 1
    Found the "missing link". The DIDD path will match the results of an EnumDisplayDevices() lookup, __if you specify EDD_GET_DEVICE_INTERFACE_NAME__ for dwFlags! Thanks for help, Hans (feel free to post an answer if you want the bounty) – holtavolt Apr 24 '12 at 17:45
  • 1
    could you please post the entire solution? – m0sa Sep 20 '12 at 14:15
  • 1
    yeah, the final solution would be pretty handy, can you post it please? – Roni Tovi Sep 18 '15 at 12:53

1 Answers1

7

A method to resolve the GDI to SetupAPI is available in the EnumDisplayDevices API. If you pass in the EDD_GET_DEVICE_INTERFACE_NAME in for dwFlags, the monitor enumeration will return DeviceID information of the form:

Monitor 0 info:
DeviceName: \\.\DISPLAY1
MonitorInfo: Dell P2411H(Digital)
DeviceID: \\?\DISPLAY#DELA06E#5&2e2fefea&0&UID1078018#{e6f07b5f-ee97-4a90-b076-3
3f57bf4eaa7}
Monitor 1 info:
DeviceName: \\.\DISPLAY2
MonitorInfo: Dell P2411H(Digital)
DeviceID: \\?\DISPLAY#DELA06E#5&2e2fefea&0&UID1078019#{e6f07b5f-ee97-4a90-b076-3
3f57bf4eaa7}

The DeviceID fields now match the results from the didd.DevicePath, as retrieved in the C# fragment below:

    Guid MonitorGUID = new Guid(Win32.GUID_DEVINTERFACE_MONITOR);

    // We start at the "root" of the device tree and look for all
    // devices that match the interface GUID of a monitor
    IntPtr h = Win32.SetupDiGetClassDevs(ref MonitorGUID, IntPtr.Zero, IntPtr.Zero, (uint)(Win32.DIGCF_PRESENT | Win32.DIGCF_DEVICEINTERFACE));
    if (h.ToInt64() != Win32.INVALID_HANDLE_VALUE)
    {
        bool Success = true;
        uint i = 0;
        while (Success)
        {
            // create a Device Interface Data structure
            Win32.SP_DEVICE_INTERFACE_DATA dia = new Win32.SP_DEVICE_INTERFACE_DATA();
            dia.cbSize = (uint)Marshal.SizeOf(dia);

            // start the enumeration 
            Success = Win32.SetupDiEnumDeviceInterfaces(h, IntPtr.Zero, ref MonitorGUID, i, ref dia);
            if (Success)
            {
                // build a DevInfo Data structure
                Win32.SP_DEVINFO_DATA da = new Win32.SP_DEVINFO_DATA();
                da.cbSize = (uint)Marshal.SizeOf(da);

                // build a Device Interface Detail Data structure
                Win32.SP_DEVICE_INTERFACE_DETAIL_DATA didd = new Win32.SP_DEVICE_INTERFACE_DETAIL_DATA();
                didd.cbSize = (uint)(4 + Marshal.SystemDefaultCharSize); // trust me :)

                // now we can get some more detailed information
                uint nRequiredSize = 0;
                uint nBytes = Win32.BUFFER_SIZE;
                if (Win32.SetupDiGetDeviceInterfaceDetail(h, ref dia, ref didd, nBytes, out nRequiredSize, ref da))
                {
                    // Now we get the InstanceID
                    IntPtr ptrInstanceBuf = Marshal.AllocHGlobal((int)nBytes);
                    Win32.CM_Get_Device_ID(da.DevInst, ptrInstanceBuf, (int)nBytes, 0);
                    string InstanceID = Marshal.PtrToStringAuto(ptrInstanceBuf);
                    Console.WriteLine("InstanceID: {0}", InstanceID );
                    Marshal.FreeHGlobal(ptrInstanceBuf);
                   
                    Console.WriteLine("DevicePath: {0}", didd.DevicePath );
                }
                i++;
            }
        }
    }
    Win32.SetupDiDestroyDeviceInfoList(h);
}

Sample Output:

InstanceID: DISPLAY\DELA06E\5&2E2FEFEA&0&UID1078018
DevicePath: \\?\display#dela06e#5&2e2fefea&0&uid1078018#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}

The DeviceName from the original EnumDisplayDevices matches the Forms.Screen.DeviceName property.

With these two pieces of information, it is now possible to read the EDID block during the SetupDIEnumDeviceInterface traversal using a fragment like the below:

private static byte[] GetMonitorEDID(IntPtr pDevInfoSet, SP_DEVINFO_DATA deviceInfoData)
{
    IntPtr hDeviceRegistryKey = SetupDiOpenDevRegKey(pDevInfoSet, ref deviceInfoData,
        DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_QUERY_VALUE);
    if (hDeviceRegistryKey == IntPtr.Zero)
    {
        throw new Exception("Failed to open a registry key for device-specific configuration information");
    }

    IntPtr ptrBuff = Marshal.AllocHGlobal((int)256);
    try
    {
        RegistryValueKind lpRegKeyType = RegistryValueKind.Binary;
        int length = 256;
        uint result = RegQueryValueEx(hDeviceRegistryKey, "EDID", 0, ref lpRegKeyType, ptrBuff, ref length);
        if (result != 0)
        {
            throw new Exception("Can not read registry value EDID for device " + deviceInfoData.ClassGuid);
        }
    }
    finally
    {
        RegCloseKey(hDeviceRegistryKey);
    }
    byte[] edidBlock = new byte[256];
    Marshal.Copy(ptrBuff, edidBlock, 0, 256);
    Marshal.FreeHGlobal(ptrBuff);
    return edidBlock;
}

Which, finally, can be parsed for the VESA descriptor blocks, as shown in the DisplayDetails.GetMonitorDetails() method in this code.

Ian Kemp
  • 26,561
  • 17
  • 107
  • 129
holtavolt
  • 4,307
  • 1
  • 24
  • 40