You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows下跨重连唯一追踪显示器的C#实现问题求助

跨设备重连场景下C#追踪Windows显示器并关联显示编号

我用C#开发软件,需要在Windows跨设备重连时唯一追踪显示器,重连后要重新获取DISPLAY1这类显示编号,传给ChangeDisplaySettingsEx接口(该接口仅支持EnumDisplayDevices返回的显示编号)。

最初思路是通过WMI的root\WMI路径查询WmiMonitorID类获取显示器序列号,再匹配EnumDisplayDevices的信息,但WMI与EnumDisplayDevices没有通用匹配字段,尝试匹配InstanceName和DeviceID均失败。

其他考量

  • EnumDisplayDevices返回的数据无显示器唯一标识,无法直接用于识别
  • WmiMonitorID中仅显示器序列号为静态唯一标识,可区分同款多显示器

以下是测试代码(能获取数据但无法建立匹配):

public class DisplayTracker
{
    private static readonly Guid GUID_DEVCLASS_MONITOR = new Guid("4d36e96e-e325-11ce-bfc1-08002be10318");

    static void Main(string[] args)
    {
        PrintDisplayNumbersAndSerials();
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct SP_DEVINFO_DATA
    {
        public int cbSize;
        public Guid ClassGuid;
        public uint DevInst;
        public IntPtr Reserved;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    private struct DISPLAY_DEVICE
    {
        public int cb;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
        public string DeviceName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
        public string DeviceString;
        public int StateFlags;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
        public string DeviceID;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
        public string DeviceKey;
    }

    [DllImport("SetupAPI.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern IntPtr SetupDiGetClassDevs(
        ref Guid ClassGuid,
        IntPtr Enumerator,
        IntPtr hwndParent,
        uint Flags
    );

    [DllImport("SetupAPI.dll", SetLastError = true)]
    private static extern bool SetupDiEnumDeviceInfo(
        IntPtr DeviceInfoSet,
        uint MemberIndex,
        ref SP_DEVINFO_DATA DeviceInfoData
    );

    [DllImport("SetupAPI.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern bool SetupDiGetDeviceInstanceId(
        IntPtr DeviceInfoSet,
        ref SP_DEVINFO_DATA DeviceInfoData,
        StringBuilder DeviceInstanceId,
        int DeviceInstanceIdSize,
        out int RequiredSize
    );

    [DllImport("SetupAPI.dll", SetLastError = true)]
    private static extern bool SetupDiDestroyDeviceInfoList(IntPtr DeviceInfoSet);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags);

    private const uint DIGCF_PRESENT = 0x00000002;

    public static void PrintDisplayNumbersAndSerials()
    {
        var serials = GetMonitorSerialsFromWMI();
        var displays = GetCurrentDisplays();
        var connectedDevices = GetConnectedDeviceInstanceIDs();

        foreach (var kvp in serials)
        {
            string instanceName = kvp.Key;
            string serial = kvp.Value;

            string matchedDeviceInstance = connectedDevices.FirstOrDefault(dev => dev.Contains(instanceName, StringComparison.OrdinalIgnoreCase));

            if (matchedDeviceInstance != null)
            {
                var match = displays.FirstOrDefault(d => matchedDeviceInstance.Contains(d.Key, StringComparison.OrdinalIgnoreCase));
                if (!string.IsNullOrEmpty(match.Value))
                {
                    Console.WriteLine($"Display Serial: {serial} -> Display Number: {match.Value}");
                }
                else
                {
                    Console.WriteLine($"Display Serial: {serial} -> Display Number: (Not Found)");
                }
            }
            else
            {
                Console.WriteLine($"Display Serial: {serial} -> Display Number: (Not Connected)");
            }
        }
    }

    public static Dictionary<string, string> GetCurrentDisplays()
    {
        var displays = new Dictionary<string, string>();

        DISPLAY_DEVICE displayDevice = new DISPLAY_DEVICE();
        displayDevice.cb = Marshal.SizeOf(displayDevice);
        uint deviceIndex = 0;

        while (EnumDisplayDevices(null, deviceIndex, ref displayDevice, 0))
        {
            displays[displayDevice.DeviceID] = displayDevice.DeviceName;
            deviceIndex++;
        }

        return displays;
    }

    public static Dictionary<string, string> GetMonitorSerialsFromWMI()
    {
        var monitorSerials = new Dictionary<string, string>();

        try
        {
            ManagementObjectSearcher searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM WmiMonitorID");

            foreach (ManagementObject mo in searcher.Get())
            {
                string instanceName = mo["InstanceName"] as string;
                string serial = GetStringFromUShortArray((ushort[])mo["SerialNumberID"]);

                if (!string.IsNullOrEmpty(instanceName) && !string.IsNullOrEmpty(serial))
                {
                    monitorSerials[instanceName] = serial;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error querying WMI: {ex.Message}");
        }

        return monitorSerials;
    }

    private static string GetStringFromUShortArray(ushort[] data)
    {
        if (data == null) return null;
        return Encoding.ASCII.GetString(Array.ConvertAll(data, Convert.ToByte)).TrimEnd('\0');
    }

    public static List<string> GetConnectedDeviceInstanceIDs()
    {
        var deviceInstanceIDs = new List<string>();

        Guid monitorGuid = GUID_DEVCLASS_MONITOR;
        IntPtr deviceInfoSet = SetupDiGetClassDevs(ref monitorGuid, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT);
        if (deviceInfoSet == IntPtr.Zero) return deviceInstanceIDs;

        try
        {
            SP_DEVINFO_DATA deviceInfoData = new SP_DEVINFO_DATA();
            deviceInfoData.cbSize = Marshal.SizeOf(deviceInfoData);

            uint index = 0;
            while (SetupDiEnumDeviceInfo(deviceInfoSet, index, ref deviceInfoData))
            {
                StringBuilder deviceInstanceId = new StringBuilder(256);
                int requiredSize;

                if (SetupDiGetDeviceInstanceId(deviceInfoSet, ref deviceInfoData, deviceInstanceId, deviceInstanceId.Capacity, out requiredSize))
                {
                    deviceInstanceIDs.Add(deviceInstanceId.ToString());
                }

                index++;
            }
        }
        finally
        {
            SetupDiDestroyDeviceInfoList(deviceInfoSet);
        }

        return deviceInstanceIDs;
    }
}

解决方案:通过设备实例ID关联序列号与显示编号

之前匹配失败的核心原因是:EnumDisplayDevices返回的DeviceID是显示适配器的ID,而非显示器本身的ID。正确的做法是提取显示器的设备实例ID,以此作为WMI与EnumDisplayDevices的关联桥梁。

修改后的核心逻辑

  1. 从WmiMonitorID的InstanceName中提取显示器的设备实例ID片段(格式如DISPLAY\LGD056B\4&1a2b3c4d&0&UID12345)
  2. 从EnumDisplayDevices返回的DeviceKey(注册表路径)中提取对应的设备实例ID片段
  3. 通过设备实例ID片段建立序列号与显示编号的映射

修改后的代码示例

public class DisplayTracker
{
    private static readonly Guid GUID_DEVCLASS_MONITOR = new Guid("4d36e96e-e325-11ce-bfc1-08002be10318");

    static void Main(string[] args)
    {
        PrintDisplayNumbersAndSerials();
    }

    // 保留原有的SP_DEVINFO_DATA、DISPLAY_DEVICE结构体及DllImport声明...

    public static void PrintDisplayNumbersAndSerials()
    {
        var serials = GetMonitorSerialsFromWMI();
        var displays = GetCurrentDisplaysWithDeviceKey();

        foreach (var kvp in serials)
        {
            string deviceInstanceIdPart = kvp.Key;
            string serial = kvp.Value;

            if (displays.TryGetValue(deviceInstanceIdPart, out string displayName))
            {
                Console.WriteLine($"Display Serial: {serial} -> Display Number: {displayName}");
            }
            else
            {
                // 模糊匹配兼容部分格式差异
                var fuzzyMatch = displays.FirstOrDefault(d => d.Key.StartsWith(deviceInstanceIdPart.Split('\\')[0] + "\\", StringComparison.OrdinalIgnoreCase));
                if (!string.IsNullOrEmpty(fuzzyMatch.Value))
                {
                    Console.WriteLine($"Display Serial: {serial} -> Display Number: {fuzzyMatch.Value} (Fuzzy Match)");
                }
                else
                {
                    Console.WriteLine($"Display Serial: {serial} -> Display Number: (Not Found)");
                }
            }
        }
    }

    public static Dictionary<string, string> GetCurrentDisplaysWithDeviceKey()
    {
        var displays = new Dictionary<string, string>();
        DISPLAY_DEVICE displayDevice = new DISPLAY_DEVICE();
        displayDevice.cb = Marshal.SizeOf(displayDevice);
        uint deviceIndex = 0;

        while (EnumDisplayDevices(null, deviceIndex, ref displayDevice, 0))
        {
            if (!string.IsNullOrEmpty(displayDevice.DeviceKey))
            {
                // 从注册表路径中提取设备实例ID片段
                string enumPath = @"SYSTEM\CurrentControlSet\Enum\";
                int enumIndex = displayDevice.DeviceKey.IndexOf(enumPath, StringComparison.OrdinalIgnoreCase);
                if (enumIndex != -1)
                {
                    string deviceInstanceIdPart = displayDevice.DeviceKey.Substring(enumIndex + enumPath.Length);
                    int controlIndex = deviceInstanceIdPart.IndexOf("\\Control", StringComparison.OrdinalIgnoreCase);
                    if (controlIndex != -1)
                    {
                        deviceInstanceIdPart = deviceInstanceIdPart.Substring(0, controlIndex);
                        displays[deviceInstanceIdPart] = displayDevice.DeviceName;
                    }
                }
            }
            deviceIndex++;
        }
        return displays;
    }

    public static Dictionary<string, string> GetMonitorSerialsFromWMI()
    {
        var monitorSerials = new Dictionary<string, string>();
        try
        {
            ManagementObjectSearcher searcher = new ManagementObjectSearcher(@"root\WMI", "SELECT * FROM WmiMonitorID");
            foreach (ManagementObject mo in searcher.Get())
            {
                string instanceName = mo["InstanceName"] as string;
                string serial = GetStringFromUShortArray((ushort[])mo["SerialNumberID"]);

                if (!string.IsNullOrEmpty(instanceName) && !string.IsNullOrEmpty(serial))
                {
                    // 提取InstanceName中的设备实例ID片段
                    int displayIndex = instanceName.IndexOf("DISPLAY", StringComparison.OrdinalIgnoreCase);
                    if (displayIndex != -1)
                    {
                        string deviceInstanceIdPart = instanceName.Substring(displayIndex).Replace(@"\\", @"\");
                        monitorSerials[deviceInstanceIdPart] = serial;
                    }
                    else
                    {
                        monitorSerials[instanceName] = serial;
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error querying WMI: {ex.Message}");
        }
        return monitorSerials;
    }

    // 保留原有的GetStringFromUShortArray、GetConnectedDeviceInstanceIDs方法...
}

说明

  • WmiMonitorID的InstanceName和EnumDisplayDevices的DeviceKey都包含显示器的唯一设备实例ID,通过提取该片段即可建立可靠关联
  • 模糊匹配逻辑用于兼容部分系统中实例ID格式的细微差异
  • 该方案能稳定支持显示器重连后的编号追踪,满足ChangeDisplaySettingsEx接口的参数需求

内容的提问来源于stack exchange,提问作者epice500

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 02:25:53