using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using Microsoft.Win32; namespace ParsecVDisplay { internal class Display { public enum Orientation { Landscape = 0, // Angle0 Portrait, // Angle90 Landscape_Flipped, // Angle180 Portrait_Flipped // Angle270 } public class Mode { public int Width; public int Height; public int Hz; public Mode() { } public Mode(int width, int height, int hz) { Width = width; Height = height; Hz = hz; } public Mode(ulong bits) { Width = unchecked((ushort)bits); Height = unchecked((ushort)(bits >> 16)); Hz = unchecked((ushort)(bits >> 32)); } public ulong Bits => (uint)(Width & 0xFFFF) | ((ulong)(Height & 0xFFFF) << 16) | ((ulong)(Hz & 0xFFFF) << 32); public string Resolution => $"{Width} × {Height}"; public string RefreshRate => $"{Hz} Hz"; public override string ToString() => $"{Resolution} @ {RefreshRate}"; } public class ModeSet { public int Width; public int Height; public List RefreshRates; } public bool Active; public int Identifier; public int CloneOf; public int Address; public DateTime LastArrival; public string Adapter; public string AdapterInstance; public DateTime AdapterArrival; public string DeviceName; public string DisplayName; public Mode CurrentMode; public Orientation CurrentOrientation; public List ModeList; public List SupportedResolutions; public int DisplayIndex => Address - 0x100; Display() { ModeList = new List(); CurrentOrientation = Orientation.Landscape; SupportedResolutions = new List(); } public override string ToString() { var str = $"[{Identifier}] {DeviceName} ({DisplayName}#{Address})"; if (CloneOf > 0 && CloneOf < Identifier) str += $" (clone of [{CloneOf}])"; return str; } void FetchAllModes() { var devMode = new Native.DEVMODE(); devMode.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE)); var set = new Dictionary>(); for (int num = -1; Native.EnumDisplaySettings(DeviceName, num, ref devMode); num++) { var mode = new Mode { Width = devMode.dmPelsWidth, Height = devMode.dmPelsHeight, Hz = devMode.dmDisplayFrequency, }; if (num == -1) { CurrentMode = mode; CurrentOrientation = devMode.dmDisplayOrientation; } else { ModeList.Add(mode); mode.Hz = 0; if (set.TryGetValue(mode.Bits, out var rrs)) { rrs.Add(devMode.dmDisplayFrequency); } else { set[mode.Bits] = new HashSet { devMode.dmDisplayFrequency }; } } } foreach (var kv in set) { var mode = new Mode(kv.Key); var rrs = kv.Value.ToList(); rrs.Sort((a, b) => a - b); SupportedResolutions.Add(new ModeSet { Width = mode.Width, Height = mode.Height, RefreshRates = rrs, }); } SupportedResolutions.Sort((a, b) => b.Width == a.Width ? (b.Height - a.Height) : (b.Width - a.Width)); } public bool ChangeMode(int? width, int? height, int? hz, Orientation? orientation) { var mode = new Native.DEVMODE(); mode.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE)); if (Native.EnumDisplaySettings(DeviceName, -1, ref mode)) { if (width.HasValue) { mode.dmPelsWidth = width.Value; mode.dmFields |= /*DM_PELSWIDTH*/ 0x80000; } if (height.HasValue) { mode.dmPelsHeight = height.Value; mode.dmFields |= /*DM_PELSHEIGHT*/ 0x100000; } if (hz.HasValue) { mode.dmDisplayFrequency = hz.Value; mode.dmFields |= /*DM_DISPLAYFREQUENCY*/ 0x400000; } if (orientation.HasValue) { var newDO = orientation.Value; mode.dmDisplayOrientation = newDO; if (((int)newDO + (int)CurrentOrientation) % 2 != 0) { int t = mode.dmPelsWidth; mode.dmPelsWidth = mode.dmPelsHeight; mode.dmPelsHeight = t; } } return Native.ChangeDisplaySettingsEx(DeviceName, ref mode, IntPtr.Zero, /*CDS_UPDATEREGISTRY*/ 0x1 | /*CDS_GLOBAL*/ /*0x8*/ 0, IntPtr.Zero) == 0; } return false; } public void TakeScreenshot(string saveFile) { var devMode = new Native.DEVMODE(); devMode.dmSize = (short)Marshal.SizeOf(typeof(Native.DEVMODE)); Native.EnumDisplaySettings(DeviceName, -1, ref devMode); int x = devMode.dmPositionX; int y = devMode.dmPositionY; int width = devMode.dmPelsWidth; int height = devMode.dmPelsHeight; using (var bmp = new Bitmap(width, height)) using (var gfx = Graphics.FromImage(bmp)) { var hdcSrc = Native.GetDC(IntPtr.Zero); var hdcDest = gfx.GetHdc(); Native.BitBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, 0x00CC0020); gfx.ReleaseHdc(hdcDest); bmp.Save(saveFile, System.Drawing.Imaging.ImageFormat.Png); Native.ReleaseDC(IntPtr.Zero, hdcSrc); } } public static string DumpModes(List modes) { return string.Join(",", modes.Select(m => m.Bits.ToString("x"))); } public static List ParseModes(string modes) { var list = new List(); var tokens = modes.Trim().Split(','); foreach (var mode in tokens) if (ulong.TryParse(mode, NumberStyles.HexNumber, null, out var bits)) list.Add(new Mode(bits)); return list; } public static List GetAllDisplays() { var displayMap = new Dictionary(StringComparer.OrdinalIgnoreCase); var cloneGroups = new List>(); var paths = GetDisplayPaths(); var dd = new Native.DISPLAY_DEVICE(); dd.cb = Marshal.SizeOf(typeof(Native.DISPLAY_DEVICE)); for (int i = 0; Native.EnumDisplayDevices(null, i, ref dd, 0); i++) { Display prevActiveDisplay = null; var dd2 = new Native.DISPLAY_DEVICE(); dd2.cb = Marshal.SizeOf(typeof(Native.DISPLAY_DEVICE)); for (int j = 0; Native.EnumDisplayDevices(dd.DeviceName, j, ref dd2, Native.EDD_GET_DEVICE_INTERFACE_NAME); j++) { if ((dd2.StateFlags & Native.DISPLAY_DEVICE_ATTACHED) == 0) continue; var pathIdx = paths.FindIndex(p => dd2.DeviceID.Contains(p.Replace('\\', '#'))); if (pathIdx < 0) continue; if (!displayMap.ContainsKey(paths[pathIdx])) { var display = new Display { Active = (dd2.StateFlags & Native.DISPLAY_DEVICE_ACTIVE) != 0, Address = ParseDisplayAddress(paths[pathIdx]), DeviceName = dd.DeviceName, DisplayName = ParseDisplayCode(dd2.DeviceID), }; if (display.Active) { if (prevActiveDisplay == null) { prevActiveDisplay = display; } else { cloneGroups.Add(new Tuple(prevActiveDisplay, display)); } display.FetchAllModes(); } Device.GetDeviceInstance(paths[pathIdx], out uint devInst); display.LastArrival = Device.GetDeviceLastArrival(devInst); Device.GetParentDeviceInstance(devInst, out uint parentInst, out display.AdapterInstance); display.Adapter = Device.GetDeviceDescription(parentInst); display.AdapterArrival = Device.GetDeviceLastArrival(parentInst); displayMap.Add(paths[pathIdx], display); paths.RemoveAt(pathIdx); } } } var displays = displayMap.Values.ToList(); // Sort displays by adapter arrival time displays.Sort((a, b) => { if (a.AdapterInstance == b.AdapterInstance) return a.DeviceName.CompareTo(b.DeviceName); return a.AdapterArrival.CompareTo(b.AdapterArrival); }); // Fill display identifier for (int i = 0; i < displays.Count; i++) displays[i].Identifier = i + 1; // Fill clone of identifier foreach (var group in cloneGroups) { group.Item1.CloneOf = group.Item2.Identifier; group.Item2.CloneOf = group.Item1.Identifier; } cloneGroups.Clear(); return displays; } static List GetDisplayPaths() { var paths = new List(); using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\monitor\Enum", false)) { if (key != null) { int count = Convert.ToInt32(key.GetValue("Count", 0)); for (int i = 0; i < count; ++i) { var path = key.GetValue($"{i}"); paths.Add(Convert.ToString(path)); } } } return paths; } static int ParseDisplayAddress(string path) { var index = path.LastIndexOf("uid", StringComparison.OrdinalIgnoreCase); int.TryParse(path.Substring(index + 3), out var address); return address; } static string ParseDisplayCode(string id) { var tokens = id.Split('#'); return tokens.Length >= 2 ? tokens[1] : tokens[0]; } static class Native { public const uint EDD_GET_DEVICE_INTERFACE_NAME = 0x1; public const uint DISPLAY_DEVICE_ACTIVE = 0x1; public const uint DISPLAY_DEVICE_ATTACHED = 0x2; [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumDisplayDevices(string lpDevice, int iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct DISPLAY_DEVICE { public int cb; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string DeviceName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceString; public uint StateFlags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceID; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceKey; } [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode); [DllImport("user32.dll")] public static extern int ChangeDisplaySettingsEx(string lpszDeviceName, ref DEVMODE lpDevMode, IntPtr hwnd, uint dwflags, IntPtr lParam); [StructLayout(LayoutKind.Sequential)] public 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 Orientation 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("gdi32.dll")] public static extern IntPtr CreateDC(IntPtr pwszDriver, string pwszDevice, IntPtr pszPort, IntPtr devmode); [DllImport("gdi32.dll")] public static extern int BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int rasterOperation); [DllImport("gdi32.dll")] public static extern int DeleteDC(IntPtr hdc); [DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hwnd); [DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); } } }