// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium; namespace Microsoft.PowerToys.UITest { /// /// Base class that should be inherited by all Test Classes. /// [TestClass] public class UITestBase : IDisposable { public required TestContext TestContext { get; set; } public required Session Session { get; set; } /// /// Gets a value indicating whether the tests are running in a CI/CD pipeline. /// public bool IsInPipeline { get; } public string? ScreenshotDirectory { get; set; } public string? RecordingDirectory { get; set; } public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List() }; private readonly PowerToysModule scope; private readonly WindowSize size; private readonly string[]? commandLineArgs; private SessionHelper? sessionHelper; private System.Threading.Timer? screenshotTimer; private ScreenRecording? screenRecording; public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null) { this.IsInPipeline = EnvironmentConfig.IsInPipeline; Console.WriteLine($"Running tests on platform: {EnvironmentConfig.Platform}"); if (IsInPipeline) { NativeMethods.ChangeDisplayResolution(1920, 1080); NativeMethods.GetMonitorInfo(); // Escape Popups before starting System.Windows.Forms.SendKeys.SendWait("{ESC}"); } this.scope = scope; this.size = size; this.commandLineArgs = commandLineArgs; } /// /// Initializes the test. /// [TestInitialize] public void TestInit() { KeyboardHelper.SendKeys(Key.Win, Key.M); CloseOtherApplications(); if (IsInPipeline) { string baseDirectory = this.TestContext.TestResultsDirectory ?? string.Empty; ScreenshotDirectory = Path.Combine(baseDirectory, "UITestScreenshots_" + Guid.NewGuid().ToString()); Directory.CreateDirectory(ScreenshotDirectory); RecordingDirectory = Path.Combine(baseDirectory, "UITestRecordings_" + Guid.NewGuid().ToString()); Directory.CreateDirectory(RecordingDirectory); // Take screenshot every 1 second screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000)); // Start screen recording (requires FFmpeg) try { screenRecording = new ScreenRecording(RecordingDirectory); if (screenRecording.IsAvailable) { _ = screenRecording.StartRecordingAsync(); } else { screenRecording = null; } } catch (Exception ex) { Console.WriteLine($"Failed to start screen recording: {ex.Message}"); screenRecording = null; } // Escape Popups before starting System.Windows.Forms.SendKeys.SendWait("{ESC}"); } this.sessionHelper = new SessionHelper(scope, commandLineArgs).Init(); this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), scope, size); } /// /// Cleanups the test. /// [TestCleanup] public void TestCleanup() { if (IsInPipeline) { screenshotTimer?.Change(Timeout.Infinite, Timeout.Infinite); // Stop screen recording if (screenRecording != null) { try { screenRecording.StopRecordingAsync().GetAwaiter().GetResult(); } catch (Exception ex) { Console.WriteLine($"Failed to stop screen recording: {ex.Message}"); } } if (TestContext.CurrentTestOutcome is UnitTestOutcome.Failed or UnitTestOutcome.Error or UnitTestOutcome.Unknown) { Task.Delay(1000).Wait(); AddScreenShotsToTestResultsDirectory(); AddRecordingsToTestResultsDirectory(); AddLogFilesToTestResultsDirectory(); } else { // Clean up recording if test passed CleanupRecordingDirectory(); } Dispose(); } this.Session.Cleanup(); this.sessionHelper!.Cleanup(); } public void Dispose() { screenshotTimer?.Dispose(); screenRecording?.Dispose(); GC.SuppressFinalize(this); } /// /// Finds an element by selector. /// Shortcut for this.Session.Find(by, timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// The found element. protected T Find(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Session.Find(by, timeoutMS, global); } /// /// Shortcut for this.Session.Find(name, timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// The found element. protected T Find(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Session.Find(By.Name(name), timeoutMS, global); } /// /// Shortcut for this.Session.Find(by, timeoutMS) /// /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// The found element. protected Element Find(By by, int timeoutMS = 5000, bool global = false) { return this.Session.Find(by, timeoutMS, global); } /// /// Shortcut for this.Session.Find(name, timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// The found element. protected Element Find(string name, int timeoutMS = 5000, bool global = false) { return this.Session.Find(name, timeoutMS, global); } /// /// Has only one Element or its derived class by selector. /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element; otherwise, false. public bool HasOne(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.FindAll(by, timeoutMS, global).Count == 1; } /// /// Shortcut for this.Session.HasOne(by, timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element; otherwise, false. public bool HasOne(By by, int timeoutMS = 5000, bool global = false) { return this.Session.HasOne(by, timeoutMS, global); } /// /// Shortcut for this.Session.HasOne(name, timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element; otherwise, false. public bool HasOne(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Session.HasOne(By.Name(name), timeoutMS, global); } /// /// Shortcut for this.Session.HasOne(name, timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if only has one element; otherwise, false. public bool HasOne(string name, int timeoutMS = 5000, bool global = false) { return this.Session.HasOne(name, timeoutMS, global); } /// /// Shortcut for this.Session.Has(by, timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element; otherwise, false. public bool Has(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Session.FindAll(by, timeoutMS, global).Count >= 1; } /// /// Shortcut for this.Session.Has(by, timeoutMS) /// /// The selector to find the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element; otherwise, false. public bool Has(By by, int timeoutMS = 5000, bool global = false) { return this.Session.Has(by, timeoutMS, global); } /// /// Shortcut for this.Session.Has(By.Name(name), timeoutMS) /// /// The class of the element, should be Element or its derived class. /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element; otherwise, false. public bool Has(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Session.Has(By.Name(name), timeoutMS, global); } /// /// Shortcut for this.Session.Has(name, timeoutMS) /// /// The name of the element. /// The timeout in milliseconds (default is 5000). /// True if has one or more element; otherwise, false. public bool Has(string name, int timeoutMS = 5000, bool global = false) { return this.Session.Has(name, timeoutMS, global); } /// /// Finds an element using partial name matching (contains). /// Useful for finding windows with variable titles like "filename.txt - Notepad" or "filename - Notepad". /// /// The class of the element, should be Element or its derived class. /// Part of the name to search for. /// The timeout in milliseconds (default is 5000). /// The found element. protected T FindByPartialName(string partialName, int timeoutMS = 5000, bool global = false) where T : Element, new() { return Session.Find(By.XPath($"//*[contains(@Name, '{partialName}')]"), timeoutMS, global); } /// /// Finds an element using partial name matching (contains). /// /// Part of the name to search for. /// The timeout in milliseconds (default is 5000). /// The found element. protected Element FindByPartialName(string partialName, int timeoutMS = 5000, bool global = false) { return FindByPartialName(partialName, timeoutMS, global); } /// /// Base method for finding elements by selector and filtering by name pattern. /// /// The class of the element, should be Element or its derived class. /// The selector to find initial candidates. /// Pattern to match against the Name attribute. Supports regex patterns. /// The timeout in milliseconds (default is 5000). /// Custom error message when no element is found. /// The found element. private T FindByNamePattern(By selector, string namePattern, int timeoutMS = 5000, bool global = false, string? errorMessage = null) where T : Element, new() { var elements = Session.FindAll(selector, timeoutMS, global); var regex = new Regex(namePattern, RegexOptions.IgnoreCase); foreach (var element in elements) { var name = element.GetAttribute("Name"); if (!string.IsNullOrEmpty(name) && regex.IsMatch(name)) { return element; } } throw new NoSuchElementException(errorMessage ?? $"No element found matching pattern: {namePattern}"); } /// /// Finds an element using regular expression pattern matching. /// /// The class of the element, should be Element or its derived class. /// Regular expression pattern to match against the Name attribute. /// The timeout in milliseconds (default is 5000). /// The found element. protected T FindByPattern(string pattern, int timeoutMS = 5000, bool global = false) where T : Element, new() { return FindByNamePattern(By.XPath("//*[@Name]"), pattern, timeoutMS, global, $"No element found matching pattern: {pattern}"); } /// /// Finds an element using regular expression pattern matching. /// /// Regular expression pattern to match against the Name attribute. /// The timeout in milliseconds (default is 5000). /// The found element. protected Element FindByPattern(string pattern, int timeoutMS = 5000, bool global = false) { return FindByPattern(pattern, timeoutMS, global); } /// /// Finds an element by ClassName only. /// Returns the first element found with the specified ClassName. /// /// The class of the element, should be Element or its derived class. /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). /// The timeout in milliseconds (default is 5000). /// The found element. protected T FindByClassName(string className, int timeoutMS = 5000, bool global = false) where T : Element, new() { return Session.Find(By.ClassName(className), timeoutMS, global); } /// /// Finds an element by ClassName only. /// Returns the first element found with the specified ClassName. /// /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). /// The timeout in milliseconds (default is 5000). /// The found element. protected Element FindByClassName(string className, int timeoutMS = 5000, bool global = false) { return FindByClassName(className, timeoutMS, global); } /// /// Finds an element by ClassName and matches its Name attribute using regex pattern matching. /// /// The class of the element, should be Element or its derived class. /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). /// Pattern to match against the Name attribute. Supports regex patterns. /// The timeout in milliseconds (default is 5000). /// The found element. protected T FindByClassNameAndNamePattern(string className, string namePattern, int timeoutMS = 5000, bool global = false) where T : Element, new() { return FindByNamePattern(By.ClassName(className), namePattern, timeoutMS, global, $"No element with ClassName '{className}' found matching name pattern: {namePattern}"); } /// /// Finds an element by ClassName and matches its Name attribute using regex pattern matching. /// /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). /// Pattern to match against the Name attribute. Supports regex patterns. /// The timeout in milliseconds (default is 5000). /// The found element. protected Element FindByClassNameAndNamePattern(string className, string namePattern, int timeoutMS = 5000, bool global = false) { return FindByClassNameAndNamePattern(className, namePattern, timeoutMS, global); } /// /// Finds a Notepad window regardless of whether the file extension is shown in the title. /// Handles both "filename.txt - Notepad" and "filename - Notepad" formats. /// Uses ClassName to efficiently find Notepad windows first, then matches the filename. /// /// The base filename without extension (e.g., "test" for "test.txt"). /// The timeout in milliseconds (default is 5000). /// The found Notepad window element. protected Element FindNotepadWindow(string baseFileName, int timeoutMS = 5000, bool global = false) { string pattern = $@"^{Regex.Escape(baseFileName)}(\.\w+)?(\s*-\s*|\s+)Notepad$"; return FindByClassNameAndNamePattern("Notepad", pattern, timeoutMS, global); } /// /// Finds an Explorer window regardless of the folder or file name display format. /// Handles various Explorer window title formats like "FolderName", "FileName", "FolderName - File Explorer", etc. /// Uses ClassName to efficiently find Explorer windows first, then matches the folder or file name. /// /// The folder or file name to search for (e.g., "Documents", "Desktop", "test.txt"). /// The timeout in milliseconds (default is 5000). /// The found Explorer window element. protected Element FindExplorerWindow(string folderName, int timeoutMS = 5000, bool global = false) { string pattern = $@"^{Regex.Escape(folderName)}(\s*-\s*(File\s+Explorer|Windows\s+Explorer))?$"; return FindByClassNameAndNamePattern("CabinetWClass", pattern, timeoutMS, global); } /// /// Finds an Explorer window by partial folder path. /// Useful when the full path might be displayed in the title. /// /// Part of the folder path to search for. /// The timeout in milliseconds (default is 5000). /// The found Explorer window element. protected Element FindExplorerByPartialPath(string partialPath, int timeoutMS = 5000, bool global = false) { return FindByPartialName(partialPath, timeoutMS, global); } /// /// Finds all elements by selector. /// Shortcut for this.Session.FindAll(by, timeoutMS) /// /// The class of the elements, should be Element or its derived class. /// The selector to find the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. protected ReadOnlyCollection FindAll(By by, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Session.FindAll(by, timeoutMS, global); } /// /// Finds all elements by selector. /// Shortcut for this.Session.FindAll(By.Name(name), timeoutMS) /// /// The class of the elements, should be Element or its derived class. /// The name of the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. protected ReadOnlyCollection FindAll(string name, int timeoutMS = 5000, bool global = false) where T : Element, new() { return this.Session.FindAll(By.Name(name), timeoutMS, global); } /// /// Finds all elements by selector. /// Shortcut for this.Session.FindAll(by, timeoutMS) /// /// The selector to find the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. protected ReadOnlyCollection FindAll(By by, int timeoutMS = 5000, bool global = false) { return this.Session.FindAll(by, timeoutMS, global); } /// /// Finds all elements by selector. /// Shortcut for this.Session.FindAll(By.Name(name), timeoutMS) /// /// The name of the elements. /// The timeout in milliseconds (default is 5000). /// A read-only collection of the found elements. protected ReadOnlyCollection FindAll(string name, int timeoutMS = 5000, bool global = false) { return this.Session.FindAll(By.Name(name), timeoutMS, global); } /// /// Scrolls the page /// /// The number of scroll attempts. /// The direction to scroll. /// Pre-action delay in milliseconds. /// Post-action delay in milliseconds. public void Scroll(int scrollCount = 5, string direction = "Up", int msPreAction = 500, int msPostAction = 500) { MouseActionType mouseAction = direction == "Up" ? MouseActionType.ScrollUp : MouseActionType.ScrollDown; for (int i = 0; i < scrollCount; i++) { Session.PerformMouseAction(mouseAction, msPreAction, msPostAction); // Ensure settings are visible } } /// /// Captures the last screenshot when the test fails. /// protected void CaptureLastScreenshot() { // Implement your screenshot capture logic here // For example, save a screenshot to a file and return the file path string screenshotPath = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "last_screenshot.png"); this.Session.Root.GetScreenshot().SaveAsFile(screenshotPath, ScreenshotImageFormat.Png); // Save screenshot to screenshotPath & upload to test attachment this.TestContext.AddResultFile(screenshotPath); } /// /// Retrieves the color of the pixel at the specified screen coordinates. /// /// The X coordinate on the screen. /// The Y coordinate on the screen. /// The color of the pixel at the specified coordinates. public Color GetPixelColor(int x, int y) { return WindowHelper.GetPixelColor(x, y); } /// /// Retrieves the color of the pixel at the specified screen coordinates as a string. /// /// The X coordinate on the screen. /// The Y coordinate on the screen. /// The color of the pixel at the specified coordinates. public string GetPixelColorString(int x, int y) { return WindowHelper.GetPixelColorString(x, y); } /// /// Gets the size of the display. /// /// /// A tuple containing the width and height of the display. /// GetDisplaySize() { return WindowHelper.GetDisplaySize(); } /// /// Sends a combination of keys. /// /// The keys to send. public void SendKeys(params Key[] keys) { this.Session.SendKeys(keys); } /// /// Sends a sequence of keys. /// /// An array of keys to send. public void SendKeySequence(params Key[] keys) { this.Session.SendKeySequence(keys); } ///         /// Gets the current position of the mouse cursor as a tuple.         ///         /// A tuple containing the X and Y coordinates of the cursor. public Tuple GetMousePosition() { return this.Session.GetMousePosition(); } /// /// Gets the screen center coordinates. /// /// (x, y) public (int CenterX, int CenterY) GetScreenCenter() { return WindowHelper.GetScreenCenter(); } public bool IsWindowOpen(string windowName) { return WindowHelper.IsWindowOpen(windowName); } ///     /// Moves the mouse cursor to the specified screen coordinates.     ///     /// The new x-coordinate of the cursor.     /// The new y-coordinate of the cursor. /// Adds screen recordings to test results directory when test fails. /// protected void AddRecordingsToTestResultsDirectory() { if (RecordingDirectory != null && Directory.Exists(RecordingDirectory)) { // Add video files (MP4) var videoFiles = Directory.GetFiles(RecordingDirectory, "*.mp4"); foreach (string file in videoFiles) { this.TestContext.AddResultFile(file); var fileInfo = new FileInfo(file); Console.WriteLine($"Added video recording: {Path.GetFileName(file)} ({fileInfo.Length / 1024 / 1024:F1} MB)"); } if (videoFiles.Length == 0) { Console.WriteLine("No video recording available (FFmpeg not found). Screenshots are still captured."); } } } /// /// Cleans up recording directory when test passes. /// private void CleanupRecordingDirectory() { if (RecordingDirectory != null && Directory.Exists(RecordingDirectory)) { try { Directory.Delete(RecordingDirectory, true); } catch (Exception ex) { Console.WriteLine($"Failed to cleanup recording directory: {ex.Message}"); } } } /// /// Copies PowerToys log files to test results directory when test fails. /// Renames files to include the directory structure after \PowerToys. /// protected void AddLogFilesToTestResultsDirectory() { try { var localAppDataLow = Path.Combine( Environment.GetEnvironmentVariable("USERPROFILE") ?? string.Empty, "AppData", "LocalLow", "Microsoft", "PowerToys"); if (Directory.Exists(localAppDataLow)) { CopyLogFilesFromDirectory(localAppDataLow, string.Empty); } var localAppData = Path.Combine( Environment.GetEnvironmentVariable("LOCALAPPDATA") ?? string.Empty, "Microsoft", "PowerToys"); if (Directory.Exists(localAppData)) { CopyLogFilesFromDirectory(localAppData, string.Empty); } } catch (Exception ex) { // Don't fail the test if log file copying fails Console.WriteLine($"Failed to copy log files: {ex.Message}"); } } /// /// Recursively copies log files from a directory and renames them with directory structure. /// /// Source directory to copy from /// Relative path from PowerToys folder private void CopyLogFilesFromDirectory(string sourceDir, string relativePath) { if (!Directory.Exists(sourceDir)) { return; } // Process log files in current directory var logFiles = Directory.GetFiles(sourceDir, "*.log"); foreach (var logFile in logFiles) { try { var fileName = Path.GetFileName(logFile); var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName); var extension = Path.GetExtension(fileName); // Create new filename with directory structure var directoryPart = string.IsNullOrEmpty(relativePath) ? string.Empty : relativePath.Replace("\\", "-") + "-"; var newFileName = $"{directoryPart}{fileNameWithoutExt}{extension}"; // Copy file to test results directory with new name var testResultsDir = TestContext.TestResultsDirectory ?? Path.GetTempPath(); var destinationPath = Path.Combine(testResultsDir, newFileName); File.Copy(logFile, destinationPath, true); TestContext.AddResultFile(destinationPath); } catch (Exception ex) { Console.WriteLine($"Failed to copy log file {logFile}: {ex.Message}"); } } // Recursively process subdirectories var subdirectories = Directory.GetDirectories(sourceDir); foreach (var subdir in subdirectories) { var dirName = Path.GetFileName(subdir); var newRelativePath = string.IsNullOrEmpty(relativePath) ? dirName : Path.Combine(relativePath, dirName); CopyLogFilesFromDirectory(subdir, newRelativePath); } } /// /// Restart scope exe. /// public Session RestartScopeExe(string? enableModules = null) { this.sessionHelper!.RestartScopeExe(enableModules); this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver(), this.scope, this.size); return Session; } /// /// Restart scope exe. /// public void ExitScopeExe() { this.sessionHelper!.ExitScopeExe(); return; } private void CloseOtherApplications() { // Close other applications var processNamesToClose = new List { "PowerToys", "PowerToys.Settings", "PowerToys.FancyZonesEditor", }; foreach (var processName in processNamesToClose) { foreach (var process in Process.GetProcessesByName(processName)) { process.Kill(); process.WaitForExit(); } } } public class NativeMethods { [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 int StateFlags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceID; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string DeviceKey; } [DllImport("user32.dll")] private static extern int EnumDisplaySettings(IntPtr deviceName, int modeNum, ref DEVMODE devMode); [DllImport("user32.dll")] private static extern int EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode); [DllImport("user32.dll", CharSet = CharSet.Ansi)] private static extern bool EnumDisplayDevices(IntPtr lpDevice, int iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, int dwFlags); [DllImport("user32.dll")] private static extern int ChangeDisplaySettings(ref DEVMODE devMode, int flags); [DllImport("user32.dll", CharSet = CharSet.Ansi)] private static extern int ChangeDisplaySettingsEx(IntPtr lpszDeviceName, ref DEVMODE lpDevMode, IntPtr hwnd, uint dwflags, IntPtr lParam); private const int DM_PELSWIDTH = 0x80000; private const int DM_PELSHEIGHT = 0x100000; public const int ENUM_CURRENT_SETTINGS = -1; public const int CDS_TEST = 0x00000002; public const int CDS_UPDATEREGISTRY = 0x01; public const int DISP_CHANGE_SUCCESSFUL = 0; public const int DISP_CHANGE_RESTART = 1; public const int DISP_CHANGE_FAILED = -1; [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 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; } public static void GetMonitorInfo() { int deviceIndex = 0; DISPLAY_DEVICE d = default(DISPLAY_DEVICE); d.cb = Marshal.SizeOf(d); Console.WriteLine("monitor list :"); while (EnumDisplayDevices(IntPtr.Zero, deviceIndex, ref d, 0)) { Console.WriteLine($"monitor {deviceIndex + 1}:"); Console.WriteLine($" name: {d.DeviceName}"); Console.WriteLine($"  string: {d.DeviceString}"); Console.WriteLine($"  ID: {d.DeviceID}"); Console.WriteLine($"  key: {d.DeviceKey}"); Console.WriteLine(); DEVMODE dm = default(DEVMODE); dm.DmSize = (short)Marshal.SizeOf(); int modeNum = 0; while (EnumDisplaySettings(d.DeviceName, modeNum, ref dm) > 0) { MonitorInfoData.Monitors.Add(new MonitorInfoData.MonitorInfoDataWrapper() { DeviceName = d.DeviceName, DeviceString = d.DeviceString, DeviceID = d.DeviceID, DeviceKey = d.DeviceKey, PelsWidth = dm.DmPelsWidth, PelsHeight = dm.DmPelsHeight, DisplayFrequency = dm.DmDisplayFrequency, }); Console.WriteLine($"  mode {modeNum}: {dm.DmPelsWidth}x{dm.DmPelsHeight} @ {dm.DmDisplayFrequency}Hz"); modeNum++; } deviceIndex++; d.cb = Marshal.SizeOf(d); // Reset the size for the next device } } public static void ChangeDisplayResolution(int PelsWidth, int PelsHeight) { Screen screen = Screen.PrimaryScreen!; if (screen.Bounds.Width == PelsWidth && screen.Bounds.Height == PelsHeight) { return; } DEVMODE devMode = default(DEVMODE); devMode.DmDeviceName = new string(new char[32]); devMode.DmFormName = new string(new char[32]); devMode.DmSize = (short)Marshal.SizeOf(); int modeNum = 0; while (EnumDisplaySettings(IntPtr.Zero, modeNum, ref devMode) > 0) { Console.WriteLine($"Mode {modeNum}: {devMode.DmPelsWidth}x{devMode.DmPelsHeight} @ {devMode.DmDisplayFrequency}Hz"); modeNum++; } devMode.DmPelsWidth = PelsWidth; devMode.DmPelsHeight = PelsHeight; int result = NativeMethods.ChangeDisplaySettings(ref devMode, NativeMethods.CDS_TEST); if (result == DISP_CHANGE_SUCCESSFUL) { result = ChangeDisplaySettings(ref devMode, CDS_UPDATEREGISTRY); if (result == DISP_CHANGE_SUCCESSFUL) { Console.WriteLine($"Changing display resolution to {devMode.DmPelsWidth}x{devMode.DmPelsHeight}"); } else { Console.WriteLine($"Failed to change display resolution. Error code: {result}"); } } else if (result == DISP_CHANGE_RESTART) { Console.WriteLine($"Changing display resolution to {devMode.DmPelsWidth}x{devMode.DmPelsHeight} requires a restart"); } else { Console.WriteLine($"Failed to change display resolution. Error code: {result}"); } } // Windows API for moving windows [DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); private const uint SWPNOSIZE = 0x0001; private const uint SWPNOZORDER = 0x0004; public static void MoveWindow(Element window, int x, int y) { var windowHandle = IntPtr.Parse(window.GetAttribute("NativeWindowHandle") ?? "0", System.Globalization.CultureInfo.InvariantCulture); if (windowHandle != IntPtr.Zero) { SetWindowPos(windowHandle, IntPtr.Zero, x, y, 0, 0, SWPNOSIZE | SWPNOZORDER); Task.Delay(500).Wait(); } } } } }