C# 获取多个显示器信息、分辨率、缩放系数

在很多应用场景中,我们需要获取当前系统的显示器信息,特别是多显示器配置下的各个显示器的分辨率、缩放系数等。例如,在开发图形设计软件、游戏或需要全屏显示的应用时,了解显示器的物理和逻辑分辨率以及缩放系数至关重要。此外,在某些情况下,我们还需要判断多显示器配置是否有效,例如要求所有显示器的缩放系数必须一致。
本文将介绍如何使用 C# 通过 Windows API 来获取显示器的详细信息,计算缩放系数,并判断多显示器配置的有效性。

1. 准备工作

我们将使用以下 Windows API 函数:
  • EnumDisplayMonitors: 枚举所有显示器。
  • GetMonitorInfo: 获取指定显示器的信息,包括设备名称和屏幕矩形。
  • EnumDisplaySettings: 获取指定显示器的显示模式,包括物理分辨率。
同时,我们需要定义一些结构体来存储从 API 获取的数据。

2. 代码实现

以下是完整的 C# 代码,包括 DisplayInfo 类(用于存储显示器信息)、DisplayHelper 类(提供获取显示器信息和判断配置有效性的方法)以及 Program 类(主程序入口)。
csharp
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace dpiTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("正在获取显示器信息...\n");

            // 获取所有显示器信息
            List<DisplayInfo> myDisplays = DisplayHelper.GetAllDisplaysInfo();

            // 遍历并打印每个显示器的信息
            foreach (var display in myDisplays)
            {
                Console.WriteLine($"==================== 显示器 {display.Index} ====================");
                Console.WriteLine($"设备名称: {display.DeviceName}");
                Console.WriteLine($"是否为主显示器: {display.IsPrimary}");
                Console.WriteLine($"物理分辨率: {display.PhysicalWidth}x{display.PhysicalHeight}");
                Console.WriteLine($"逻辑分辨率: {display.LogicalWidth}x{display.LogicalHeight}");
                Console.WriteLine($"缩放系数 (X/Y): {display.ScaleX:F2} / {display.ScaleY:F2}");
                Console.WriteLine($"缩放百分比 (X/Y): {display.ScalePercentageX:F0}% / {display.ScalePercentageY:F0}%");
                Console.WriteLine("==============================================================");
                Console.WriteLine();
            }

            // 判断显示配置是否有效
            bool isConfigurationValid = DisplayHelper.IsDisplayConfigurationValid(myDisplays);

            // 根据判断结果输出信息
            if (isConfigurationValid)
            {
                Console.WriteLine("结论:当前显示配置有效,可以继续执行。");
                // ... 在这里执行你的业务逻辑 ...
            }
            else
            {
                Console.WriteLine("结论:当前显示配置无效,请调整显示器缩放设置。");
            }

            Console.WriteLine("\n按任意键退出...");
            Console.ReadKey();
        }

        /// <summary>
        /// 显示器信息类,用于存储单个显示器的详细信息
        /// </summary>
        public class DisplayInfo
        {
            public int Index { get; set; } // 显示器索引
            public bool IsPrimary { get; set; } // 是否为主显示器
            public string DeviceName { get; set; } // 设备名称
            public int PhysicalWidth { get; set; } // 物理分辨率宽度
            public int PhysicalHeight { get; set; } // 物理分辨率高度
            public int LogicalWidth { get; set; } // 逻辑分辨率宽度
            public int LogicalHeight { get; set; } // 逻辑分辨率高度
            public float ScaleX { get; set; } // X 方向缩放系数
            public float ScaleY { get; set; } // Y 方向缩放系数
            public float ScalePercentageX => ScaleX * 100; // X 方向缩放百分比
            public float ScalePercentageY => ScaleY * 100; // Y 方向缩放百分比
        }

        /// <summary>
        /// 显示器帮助类,提供获取显示器信息和判断配置有效性的方法
        /// </summary>
        public class DisplayHelper
        {
            // 为了获取设备名称,需要额外引入几个API
            [DllImport("user32.dll", CharSet = CharSet.Auto)]
            private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);

            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            private struct MONITORINFOEX
            {
                public int cbSize;
                public RECT rcMonitor;
                public RECT rcWork;
                public uint dwFlags;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
                public string szDevice;
            }

            [StructLayout(LayoutKind.Sequential)]
            private struct RECT
            {
                public int Left;
                public int Top;
                public int Right;
                public int Bottom;
            }

            [StructLayout(LayoutKind.Sequential)]
            private struct DEVMODE
            {
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
                public string dmDeviceName;
                public short dmSpecVersion;
                public short dmDriverVersion;
                public short dmSize;
                public short dmDriverExtra;
                public int dmFields;
                public int dmPositionX;
                public int dmPositionY;
                public int dmDisplayOrientation;
                public int dmDisplayFixedOutput;
                public short dmColor;
                public short dmDuplex;
                public short dmYResolution;
                public short dmTTOption;
                public short dmCollate;
                [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
                public string dmFormName;
                public short dmLogPixels;
                public int dmBitsPerPel;
                public int dmPelsWidth;
                public int dmPelsHeight;
                public int dmDisplayFlags;
                public int dmDisplayFrequency;
                public int dmICMMethod;
                public int dmICMIntent;
                public int dmMediaType;
                public int dmDitherType;
                public int dmReserved1;
                public int dmReserved2;
                public int dmPanningWidth;
                public int dmPanningHeight;
            }

            [DllImport("user32.dll")]
            private static extern bool EnumDisplaySettings(string lpszDeviceName, int iModeNum, ref DEVMODE lpDevMode);

            [DllImport("shcore.dll")]
            private static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);

            [DllImport("user32.dll")]
            private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsProc lpfnEnum, IntPtr dwData);

            private delegate bool EnumMonitorsProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitorPtr, IntPtr dwData);

            /// <summary>
            /// 获取所有显示器的详细信息
            /// </summary>
            /// <returns>返回包含所有显示器信息的 DisplayInfo 列表</returns>
            public static List<DisplayInfo> GetAllDisplaysInfo()
            {
                List<DisplayInfo> displays = new List<DisplayInfo>();
                int index = 1;

                // 枚举所有显示器
                EnumMonitorsProc callback = (hMonitor, hdcMonitor, lprcMonitorPtr, dwData) =>
                {
                    DisplayInfo info = new DisplayInfo();
                    info.Index = index++;

                    // 1. 获取显示器设备名称和逻辑分辨率
                    MONITORINFOEX monitorInfo = new MONITORINFOEX();
                    monitorInfo.cbSize = Marshal.SizeOf(monitorInfo);
                    if (GetMonitorInfo(hMonitor, ref monitorInfo))
                    {
                        info.DeviceName = monitorInfo.szDevice;
                        info.IsPrimary = (monitorInfo.dwFlags & 1) != 0; // 判断是否为主显示器

                        RECT rect = monitorInfo.rcMonitor;
                        info.LogicalWidth = rect.Right - rect.Left;
                        info.LogicalHeight = rect.Bottom - rect.Top;
                    }

                    // 2. 使用设备名称获取物理分辨率
                    if (!string.IsNullOrEmpty(info.DeviceName))
                    {
                        DEVMODE devMode = new DEVMODE();
                        devMode.dmSize = (short)Marshal.SizeOf(devMode);
                        // 使用 -1 (ENUM_CURRENT_SETTINGS) 获取当前生效的分辨率
                        if (EnumDisplaySettings(info.DeviceName, -1, ref devMode))
                        {
                            info.PhysicalWidth = devMode.dmPelsWidth;
                            info.PhysicalHeight = devMode.dmPelsHeight;
                        }
                    }

                    // 3. 计算缩放系数(物理分辨率 / 逻辑分辨率)
                    if (info.PhysicalWidth > 0 && info.LogicalWidth > 0)
                    {
                        info.ScaleX = (float)Math.Round((double)info.PhysicalWidth / info.LogicalWidth, 2);
                        info.ScaleY = (float)Math.Round((double)info.PhysicalHeight / info.LogicalHeight, 2);
                    }
                    else
                    {
                        info.ScaleX = 1.0f;
                        info.ScaleY = 1.0f;
                    }

                    displays.Add(info);
                    return true;
                };

                EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, callback, IntPtr.Zero);

                return displays;
            }

            /// <summary>
            /// 根据显示器数量和缩放系数判断配置是否有效。
            /// 规则:
            /// 1. 单个显示器 -> 有效 (true)
            /// 2. 两个显示器且缩放系数一致 -> 有效 (true)
            /// 3. 两个显示器且缩放系数不一致 -> 无效 (false)
            /// 4. 超过两个显示器 -> 无效 (false)
            /// </summary>
            /// <param name="displaysInfo">由 GetAllDisplaysInfo 方法获取的显示器信息列表</param>
            /// <returns>配置是否有效的布尔值</returns>
            public static bool IsDisplayConfigurationValid(List<DisplayInfo> displaysInfo)
            {
                if (displaysInfo == null)
                {
                    throw new ArgumentNullException(nameof(displaysInfo), "显示器信息列表不能为空。");
                }

                // 1. 如果只有一个显示器,直接返回 true
                if (displaysInfo.Count == 1)
                {
                    return true;
                }
                // 2. 如果有两个显示器
                else if (displaysInfo.Count == 2)
                {
                    DisplayInfo display1 = displaysInfo[0];
                    DisplayInfo display2 = displaysInfo[1];

                    // 检查两个显示器的缩放系数是否一致(X 和 Y 都要比较)
                    // 使用 Math.Abs 并设置一个容差(例如 0.01)来处理浮点数精度问题
                    bool isScaleXEqual = Math.Abs(display1.ScaleX - display2.ScaleX) < 0.01;
                    bool isScaleYEqual = Math.Abs(display1.ScaleY - display2.ScaleY) < 0.01;

                    return isScaleXEqual && isScaleYEqual;
                }
                // 3. 处理超过两个显示器的情况
                else
                {
                    return false; // 超过两个显示器,配置无效
                }
            }
        }
    }
}
 

3. 代码解析

3.1 DisplayInfo 类

这个类用于存储单个显示器的信息,包括:
  • Index: 显示器的索引(从 1 开始)。
  • IsPrimary: 是否为主显示器。
  • DeviceName: 显示器的设备名称(例如 \\.\DISPLAY1)。
  • PhysicalWidth 和 PhysicalHeight: 显示器的物理分辨率。
  • LogicalWidth 和 LogicalHeight: 显示器的逻辑分辨率(缩放后的分辨率)。
  • ScaleX 和 ScaleY: X 和 Y 方向的缩放系数(物理分辨率 / 逻辑分辨率)。
  • ScalePercentageX 和 ScalePercentageY: X 和 Y 方向的缩放百分比(缩放系数 * 100)。

3.2 DisplayHelper 类

这个类提供了两个主要方法:GetAllDisplaysInfo 和 IsDisplayConfigurationValid
3.2.1 GetAllDisplaysInfo 方法
这个方法用于获取所有显示器的详细信息,具体步骤如下:
  1. 使用 EnumDisplayMonitors 枚举所有显示器。
  2. 对于每个显示器,使用 GetMonitorInfo 获取其设备名称和屏幕矩形(逻辑分辨率)。
  3. 使用 EnumDisplaySettings 和设备名称获取该显示器的物理分辨率。
  4. 计算缩放系数(物理分辨率 / 逻辑分辨率),并保留两位小数。
  5. 将所有信息存储在 DisplayInfo 对象中,并添加到列表中返回。
3.2.2 IsDisplayConfigurationValid 方法
这个方法用于判断当前的显示配置是否有效,具体规则如下:
  • 如果只有一个显示器,返回 true
  • 如果有两个显示器,检查它们的缩放系数是否一致(X 和 Y 方向都要比较),如果一致返回 true,否则返回 false
  • 如果超过两个显示器,返回 false
在比较缩放系数时,我们使用了 Math.Abs 并设置了一个容差(0.01),以处理浮点数精度问题。

4. 运行结果

运行上述代码,你将看到类似以下的输出:
plaintext
 
 
正在获取显示器信息...

==================== 显示器 1 ====================
设备名称: \\.\DISPLAY1
是否为主显示器: True
物理分辨率: 1920x1080
逻辑分辨率: 1920x1080
缩放系数 (X/Y): 1.00 / 1.00
缩放百分比 (X/Y): 100% / 100%
==============================================================

==================== 显示器 2 ====================
设备名称: \\.\DISPLAY2
是否为主显示器: False
物理分辨率: 2560x1440
逻辑分辨率: 1707x960
缩放系数 (X/Y): 1.50 / 1.50
缩放百分比 (X/Y): 150% / 150%
==============================================================

结论:当前显示配置无效,请调整显示器缩放设置。

按任意键退出...
 
在这个例子中,有两个显示器,它们的缩放系数不一致(一个是 100%,另一个是 150%),所以显示配置无效。

5. 总结

本文介绍了如何使用 C# 通过 Windows API 来获取显示器的详细信息,包括物理分辨率、逻辑分辨率和缩放系数,并判断多显示器配置的有效性。这对于开发需要适配不同显示配置的应用程序非常有用。
你可以根据自己的需求修改 IsDisplayConfigurationValid 方法中的判断规则,例如允许超过两个显示器但要求所有显示器的缩放系数都一致。
 
posted @ 2025-11-24 16:59  卖雨伞的小男孩  阅读(0)  评论(0)    收藏  举报