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的关联桥梁。
修改后的核心逻辑
- 从
WmiMonitorID的InstanceName中提取显示器的设备实例ID片段(格式如DISPLAY\LGD056B\4&1a2b3c4d&0&UID12345) - 从
EnumDisplayDevices返回的DeviceKey(注册表路径)中提取对应的设备实例ID片段 - 通过设备实例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
相关产品推荐
相关产品推荐

