using GreenshotPlugin.Core.Enums; using GreenshotPlugin.UnmanagedHelpers; using log4net; using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace GreenshotPlugin.Core { /// /// This handles DPI changes see /// Writing DPI-Aware Desktop and Win32 Applications /// public static class DpiHelper { private static readonly ILog Log = LogManager.GetLogger(typeof(DpiHelper)); /// /// This is the default DPI for the screen /// public const uint DefaultScreenDpi = 96; /// /// Retrieve the current DPI for the UI element which is related to this DpiHandler /// public static uint Dpi { get; private set; } = GetDpiForSystem(); /// /// Calculate a DPI scale factor /// /// uint /// double public static float DpiScaleFactor(uint dpi) { return (float)dpi / DefaultScreenDpi; } /// /// Scale the supplied number according to the supplied dpi /// /// double with e.g. the width 16 for 16x16 images /// current dpi, normal is 96. /// A function which can modify the scale factor /// double with the scaled number public static float ScaleWithDpi(float someNumber, uint dpi, Func scaleModifier = null) { var dpiScaleFactor = DpiScaleFactor(dpi); if (scaleModifier != null) { dpiScaleFactor = scaleModifier(dpiScaleFactor); } return dpiScaleFactor * someNumber; } /// /// Scale the supplied number according to the supplied dpi /// /// int with e.g. 16 for 16x16 images /// current dpi, normal is 96. /// A function which can modify the scale factor /// Scaled width public static int ScaleWithDpi(int number, uint dpi, Func scaleModifier = null) { var dpiScaleFactor = DpiScaleFactor(dpi); if (scaleModifier != null) { dpiScaleFactor = scaleModifier(dpiScaleFactor); } return (int)(dpiScaleFactor * number); } /// /// Scale the supplied Size according to the supplied dpi /// /// Size to resize /// current dpi, normal is 96. /// A function which can modify the scale factor /// NativeSize scaled public static Size ScaleWithDpi(Size size, uint dpi, Func scaleModifier = null) { var dpiScaleFactor = DpiScaleFactor(dpi); if (scaleModifier != null) { dpiScaleFactor = scaleModifier(dpiScaleFactor); } return new Size((int)(dpiScaleFactor * size.Width), (int)(dpiScaleFactor * size.Height)); } /// /// Scale the supplied NativePoint according to the supplied dpi /// /// NativePoint to resize /// current dpi, normal is 96. /// A function which can modify the scale factor /// NativePoint scaled public static Point ScaleWithDpi(Point size, uint dpi, Func scaleModifier = null) { var dpiScaleFactor = DpiScaleFactor(dpi); if (scaleModifier != null) { dpiScaleFactor = scaleModifier(dpiScaleFactor); } return new Point((int)(dpiScaleFactor * size.X), (int)(dpiScaleFactor * size.Y)); } /// /// Scale the supplied NativeSizeFloat according to the supplied dpi /// /// PointF /// current dpi, normal is 96. /// A function which can modify the scale factor /// PointF public static PointF ScaleWithDpi(PointF point, uint dpi, Func scaleModifier = null) { var dpiScaleFactor = DpiScaleFactor(dpi); if (scaleModifier != null) { dpiScaleFactor = scaleModifier(dpiScaleFactor); } return new PointF(dpiScaleFactor * point.X, dpiScaleFactor * point.Y); } /// /// Scale the supplied NativeSizeFloat according to the supplied dpi /// /// NativeSizeFloat to resize /// current dpi, normal is 96. /// A function which can modify the scale factor /// NativeSize scaled public static SizeF ScaleWithDpi(SizeF size, uint dpi, Func scaleModifier = null) { var dpiScaleFactor = DpiScaleFactor(dpi); if (scaleModifier != null) { dpiScaleFactor = scaleModifier(dpiScaleFactor); } return new SizeF(dpiScaleFactor * size.Width, dpiScaleFactor * size.Height); } /// /// Scale the supplied number to the current dpi /// /// double with e.g. a width like 16 for 16x16 images /// A function which can modify the scale factor /// double with scaled number public static float ScaleWithCurrentDpi(float someNumber, Func scaleModifier = null) { return ScaleWithDpi(someNumber, Dpi, scaleModifier); } /// /// Scale the supplied number to the current dpi /// /// int with e.g. a width like 16 for 16x16 images /// A function which can modify the scale factor /// int with scaled number public static int ScaleWithCurrentDpi(int someNumber, Func scaleModifier = null) { return ScaleWithDpi(someNumber, Dpi, scaleModifier); } /// /// Scale the supplied NativeSize to the current dpi /// /// NativeSize to scale /// A function which can modify the scale factor /// NativeSize scaled public static Size ScaleWithCurrentDpi(Size size, Func scaleModifier = null) { return ScaleWithDpi(size, Dpi, scaleModifier); } /// /// Scale the supplied NativeSizeFloat to the current dpi /// /// NativeSizeFloat to scale /// A function which can modify the scale factor /// NativeSizeFloat scaled public static SizeF ScaleWithCurrentDpi(SizeF size, Func scaleModifier = null) { return ScaleWithDpi(size, Dpi, scaleModifier); } /// /// Scale the supplied NativePoint to the current dpi /// /// NativePoint to scale /// A function which can modify the scale factor /// NativePoint scaled public static Point ScaleWithCurrentDpi(Point point, Func scaleModifier = null) { return ScaleWithDpi(point, Dpi, scaleModifier); } /// /// Scale the supplied PointF to the current dpi /// /// PointF to scale /// A function which can modify the scale factor /// PointF scaled public static PointF ScaleWithCurrentDpi(PointF point, Func scaleModifier = null) { return ScaleWithDpi(point, Dpi, scaleModifier); } /// /// Calculate a DPI unscale factor /// /// uint /// float public static float DpiUnscaleFactor(uint dpi) { return (float)DefaultScreenDpi / dpi; } /// /// Unscale the supplied number according to the supplied dpi /// /// double with e.g. the scaled width /// current dpi, normal is 96. /// A function which can modify the scale factor /// double with the unscaled number public static float UnscaleWithDpi(float someNumber, uint dpi, Func scaleModifier = null) { var dpiUnscaleFactor = DpiUnscaleFactor(dpi); if (scaleModifier != null) { dpiUnscaleFactor = scaleModifier(dpiUnscaleFactor); } return dpiUnscaleFactor * someNumber; } /// /// Unscale the supplied number according to the supplied dpi /// /// int with a scaled width /// current dpi, normal is 96. /// A function which can modify the scale factor /// Unscaled width public static int UnscaleWithDpi(int number, uint dpi, Func scaleModifier = null) { var dpiUnscaleFactor = DpiUnscaleFactor(dpi); if (scaleModifier != null) { dpiUnscaleFactor = scaleModifier(dpiUnscaleFactor); } return (int)(dpiUnscaleFactor * number); } /// /// Unscale the supplied NativeSize according to the supplied dpi /// /// NativeSize to unscale /// current dpi, normal is 96. /// A function which can modify the scale factor /// Size unscaled public static Size UnscaleWithDpi(Size size, uint dpi, Func scaleModifier = null) { var dpiUnscaleFactor = DpiUnscaleFactor(dpi); if (scaleModifier != null) { dpiUnscaleFactor = scaleModifier(dpiUnscaleFactor); } return new Size((int)(dpiUnscaleFactor * size.Width), (int)(dpiUnscaleFactor * size.Height)); } /// /// Unscale the supplied Point according to the supplied dpi /// /// Point to unscale /// current dpi, normal is 96. /// A function which can modify the scale factor /// Point unscaled public static Point UnscaleWithDpi(Point point, uint dpi, Func scaleModifier = null) { var dpiUnscaleFactor = DpiUnscaleFactor(dpi); if (scaleModifier != null) { dpiUnscaleFactor = scaleModifier(dpiUnscaleFactor); } return new Point((int)(dpiUnscaleFactor * point.X), (int)(dpiUnscaleFactor * point.Y)); } /// /// unscale the supplied NativeSizeFloat according to the supplied dpi /// /// NativeSizeFloat to resize /// current dpi, normal is 96. /// A function which can modify the scale factor /// SizeF unscaled public static SizeF UnscaleWithDpi(SizeF size, uint dpi, Func scaleModifier = null) { float dpiUnscaleFactor = DpiUnscaleFactor(dpi); if (scaleModifier != null) { dpiUnscaleFactor = scaleModifier(dpiUnscaleFactor); } return new SizeF(dpiUnscaleFactor * size.Width, dpiUnscaleFactor * size.Height); } /// /// Unscale the supplied number to the current dpi /// /// double with e.g. a width like 16 for 16x16 images /// A function which can modify the scale factor /// double with unscaled number public static float UnscaleWithCurrentDpi(float someNumber, Func scaleModifier = null) { return UnscaleWithDpi(someNumber, Dpi, scaleModifier); } /// /// Unscale the supplied number to the current dpi /// /// int with e.g. a width like 16 for 16x16 images /// A function which can modify the scale factor /// int with unscaled number public static int UnscaleWithCurrentDpi(int someNumber, Func scaleModifier = null) { return UnscaleWithDpi(someNumber, Dpi, scaleModifier); } /// /// Unscale the supplied NativeSize to the current dpi /// /// Size to unscale /// A function which can modify the scale factor /// Size unscaled public static Size UnscaleWithCurrentDpi(Size size, Func scaleModifier = null) { return UnscaleWithDpi(size, Dpi, scaleModifier); } /// /// Unscale the supplied NativeSizeFloat to the current dpi /// /// NativeSizeFloat to unscale /// A function which can modify the scale factor /// NativeSizeFloat unscaled public static SizeF UnscaleWithCurrentDpi(SizeF size, Func scaleModifier = null) { return UnscaleWithDpi(size, Dpi, scaleModifier); } /// /// Unscale the supplied NativePoint to the current dpi /// /// NativePoint to unscale /// A function which can modify the scale factor /// NativePoint unscaled public static Point UnscaleWithCurrentDpi(Point point, Func scaleModifier = null) { return UnscaleWithDpi(point, Dpi, scaleModifier); } /// /// Unscale the supplied NativePointFloat to the current dpi /// /// NativePointFloat to unscale /// A function which can modify the scale factor /// NativePointFloat unscaled public static PointF UnscaleWithCurrentDpi(PointF point, Func scaleModifier = null) { return ScaleWithDpi(point, Dpi, scaleModifier); } /// /// public wrapper for EnableNonClientDpiScaling, this also checks if the function is available. /// /// IntPtr /// true if it worked public static bool TryEnableNonClientDpiScaling(IntPtr hWnd) { // EnableNonClientDpiScaling is only available on Windows 10 and later if (!WindowsVersion.IsWindows10OrLater) { return false; } var result = EnableNonClientDpiScaling(hWnd); if (result.Succeeded()) { return true; } var error = Win32.GetLastErrorCode(); if (Log.IsDebugEnabled) { Log.DebugFormat("Error enabling non client dpi scaling : {0}", Win32.GetMessage(error)); } return false; } /// /// Make the current process DPI Aware, this should be done via the manifest but sometimes this is not possible. /// /// bool true if it was possible to change the DPI awareness public static bool EnableDpiAware() { // We can only test this for Windows 8.1 or later if (!WindowsVersion.IsWindows81OrLater) { Log.Debug("An application can only be DPI aware starting with Window 8.1 and later."); return false; } if (WindowsVersion.IsWindows10BuildOrLater(15063)) { if (IsValidDpiAwarenessContext(DpiAwarenessContext.PerMonitorAwareV2)) { SetProcessDpiAwarenessContext(DpiAwarenessContext.PerMonitorAwareV2); } else { SetProcessDpiAwarenessContext(DpiAwarenessContext.PerMonitorAwareV2); } return true; } return SetProcessDpiAwareness(DpiAwareness.PerMonitorAware).Succeeded(); } /// /// Check if the process is DPI Aware, an DpiHandler doesn't make sense if not. /// public static bool IsDpiAware { get { // We can only test this for Windows 8.1 or later if (!WindowsVersion.IsWindows81OrLater) { Log.Debug("An application can only be DPI aware starting with Window 8.1 and later."); return false; } using var process = Process.GetCurrentProcess(); GetProcessDpiAwareness(process.Handle, out var dpiAwareness); if (Log.IsDebugEnabled) { Log.DebugFormat("Process {0} has a Dpi awareness {1}", process.ProcessName, dpiAwareness); } return dpiAwareness != DpiAwareness.Unaware && dpiAwareness != DpiAwareness.Invalid; } } /// /// Retrieve the DPI value for the supplied window handle /// /// IntPtr /// dpi value public static uint GetDpi(IntPtr hWnd) { if (!User32.IsWindow(hWnd)) { return DefaultScreenDpi; } // Use the easiest method, but this only works for Windows 10 if (WindowsVersion.IsWindows10OrLater) { return GetDpiForWindow(hWnd); } // Use the second easiest method, but this only works for Windows 8.1 or later if (WindowsVersion.IsWindows81OrLater) { var hMonitor = User32.MonitorFromWindow(hWnd, MonitorFrom.DefaultToNearest); // ReSharper disable once UnusedVariable if (GetDpiForMonitor(hMonitor, MonitorDpiType.EffectiveDpi, out var dpiX, out var dpiY)) { return dpiX; } } // Fallback to the global DPI settings using var hdc = SafeWindowDcHandle.FromWindow(hWnd); if (hdc == null) { return DefaultScreenDpi; } return (uint)GDI32.GetDeviceCaps(hdc, DeviceCaps.LOGPIXELSX); } /// /// See details GetProcessDpiAwareness function /// Retrieves the dots per inch (dpi) awareness of the specified process. /// /// IntPtr with handle of the process that is being queried. If this parameter is NULL, the current process is queried. /// out DpiAwareness - The DPI awareness of the specified process. Possible values are from the PROCESS_DPI_AWARENESS enumeration. /// HResult [DllImport("shcore")] private static extern HResult GetProcessDpiAwareness(IntPtr processHandle, out DpiAwareness value); /// /// Sets the current process to a specified dots per inch (dpi) awareness level. The DPI awareness levels are from the PROCESS_DPI_AWARENESS enumeration. /// See SetProcessDpiAwareness function /// /// DpiAwareness /// HResult [DllImport("shcore")] private static extern HResult SetProcessDpiAwareness(DpiAwareness dpiAwareness); /// /// It is recommended that you set the process-default DPI awareness via application manifest. See Setting the default DPI awareness for a process for more information. Setting the process-default DPI awareness via API call can lead to unexpected application behavior. /// /// Sets the current process to a specified dots per inch (dpi) awareness context. The DPI awareness contexts are from the DPI_AWARENESS_CONTEXT value. /// Remarks: /// This API is a more advanced version of the previously existing SetProcessDpiAwareness API, allowing for the process default to be set to the finer-grained DPI_AWARENESS_CONTEXT values. Most importantly, this allows you to programmatically set Per Monitor v2 as the process default value, which is not possible with the previous API. /// /// This method sets the default DPI_AWARENESS_CONTEXT for all threads within an application. Individual threads can have their DPI awareness changed from the default with the SetThreadDpiAwarenessContext method. /// See SetProcessDpiAwarenessContext function /// /// DpiAwarenessContext /// bool [DllImport("User32.dll", SetLastError = true)] private static extern bool SetProcessDpiAwarenessContext(DpiAwarenessContext dpiAwarenessContext); /// /// See more at GetDpiForWindow function /// Returns the dots per inch (dpi) value for the associated window. /// /// IntPtr /// uint with dpi [DllImport("User32.dll")] private static extern uint GetDpiForWindow(IntPtr hWnd); /// /// See /// GetDpiForMonitor function /// Queries the dots per inch (dpi) of a display. /// /// IntPtr /// MonitorDpiType /// out int for the horizontal dpi /// out int for the vertical dpi /// true if all okay [DllImport("shcore")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetDpiForMonitor(IntPtr hMonitor, MonitorDpiType dpiType, out uint dpiX, out uint dpiY); /// /// See EnableNonClientDpiScaling function /// /// IntPtr /// bool [DllImport("User32.dll", SetLastError = true)] private static extern HResult EnableNonClientDpiScaling(IntPtr hWnd); /// /// See GetDpiForSystem function /// Returns the system DPI. /// /// uint with the system DPI [DllImport("User32.dll")] private static extern uint GetDpiForSystem(); /// /// Converts a point in a window from logical coordinates into physical coordinates, regardless of the dots per inch (dpi) awareness of the caller. For more information about DPI awareness levels, see PROCESS_DPI_AWARENESS. /// See more at LogicalToPhysicalPointForPerMonitorDPI function /// /// IntPtr A handle to the window whose transform is used for the conversion. /// A pointer to a POINT structure that specifies the logical coordinates to be converted. The new physical coordinates are copied into this structure if the function succeeds. /// bool [DllImport("User32.dll")] private static extern bool LogicalToPhysicalPointForPerMonitorDPI(IntPtr hWnd, ref POINT point); /// /// Converts a point in a window from logical coordinates into physical coordinates, regardless of the dots per inch (dpi) awareness of the caller. For more information about DPI awareness levels, see PROCESS_DPI_AWARENESS. /// See more at PhysicalToLogicalPointForPerMonitorDPI function /// /// IntPtr A handle to the window whose transform is used for the conversion. /// NativePoint A pointer to a POINT structure that specifies the physical/screen coordinates to be converted. The new logical coordinates are copied into this structure if the function succeeds. /// bool [DllImport("User32.dll")] private static extern bool PhysicalToLogicalPointForPerMonitorDPI(IntPtr hWnd, ref POINT point); /// /// See SystemParametersInfo function /// Retrieves the value of one of the system-wide parameters, taking into account the provided DPI value. /// /// /// SystemParametersInfoActions The system-wide parameter to be retrieved. /// This function is only intended for use with SPI_GETICONTITLELOGFONT, SPI_GETICONMETRICS, or SPI_GETNONCLIENTMETRICS. See SystemParametersInfo for more information on these values. /// /// /// A parameter whose usage and format depends on the system parameter being queried or set. For more /// information about system-wide parameters, see the uiAction parameter. If not otherwise indicated, you must specify /// zero for this parameter. /// /// IntPtr /// SystemParametersInfoBehaviors /// uint with dpi value /// bool [DllImport("User32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SystemParametersInfoForDpi(SystemParametersInfoActions uiAction, uint uiParam, IntPtr pvParam, SystemParametersInfoBehaviors fWinIni, uint dpi); /// /// See GetThreadDpiAwarenessContext function /// Gets the DPI_AWARENESS_CONTEXT for the current thread. /// /// This method will return the latest DPI_AWARENESS_CONTEXT sent to SetThreadDpiAwarenessContext. If SetThreadDpiAwarenessContext was never called for this thread, then the return value will equal the default DPI_AWARENESS_CONTEXT for the process. /// /// DpiAwarenessContext [DllImport("User32.dll")] private static extern DpiAwarenessContext GetThreadDpiAwarenessContext(); /// /// Set the DPI awareness for the current thread to the provided value. /// /// DpiAwarenessContext the new value for the current thread /// DpiAwarenessContext previous value [DllImport("User32.dll")] private static extern DpiAwarenessContext SetThreadDpiAwarenessContext(DpiAwarenessContext dpiAwarenessContext); /// /// Retrieves the DpiAwareness value from a DpiAwarenessContext. /// /// DpiAwarenessContext /// DpiAwareness [DllImport("User32.dll")] private static extern DpiAwareness GetAwarenessFromDpiAwarenessContext(DpiAwarenessContext dpiAwarenessContext); /// /// Retrieves the DPI from a given DPI_AWARENESS_CONTEXT handle. This enables you to determine the DPI of a thread without needed to examine a window created within that thread. /// /// DpiAwarenessContext /// uint with dpi value [DllImport("User32.dll")] private static extern uint GetDpiFromDpiAwarenessContext(DpiAwarenessContext dpiAwarenessContext); /// /// Determines if a specified DPI_AWARENESS_CONTEXT is valid and supported by the current system. /// /// DpiAwarenessContext The context that you want to determine if it is supported. /// bool true if supported otherwise false [DllImport("User32.dll")] private static extern bool IsValidDpiAwarenessContext(DpiAwarenessContext dpiAwarenessContext); /// /// Returns the DPI_HOSTING_BEHAVIOR of the specified window. /// /// This API allows you to examine the hosting behavior of a window after it has been created. A window's hosting behavior is the hosting behavior of the thread in which the window was created, as set by a call to SetThreadDpiHostingBehavior. This is a permanent value and cannot be changed after the window is created, even if the thread's hosting behavior is changed. /// /// DpiHostingBehavior [DllImport("User32.dll")] private static extern DpiHostingBehavior GetWindowDpiHostingBehavior(); /// /// See more at SetThreadDpiHostingBehavior function /// Sets the thread's DPI_HOSTING_BEHAVIOR. This behavior allows windows created in the thread to host child windows with a different DPI_AWARENESS_CONTEXT. /// /// DPI_HOSTING_BEHAVIOR enables a mixed content hosting behavior, which allows parent windows created in the thread to host child windows with a different DPI_AWARENESS_CONTEXT value. This property only effects new windows created within this thread while the mixed hosting behavior is active. A parent window with this hosting behavior is able to host child windows with different DPI_AWARENESS_CONTEXT values, regardless of whether the child windows have mixed hosting behavior enabled. /// /// This hosting behavior does not allow for windows with per-monitor DPI_AWARENESS_CONTEXT values to be hosted until windows with DPI_AWARENESS_CONTEXT values of system or unaware. /// /// To avoid unexpected outcomes, a thread's DPI_HOSTING_BEHAVIOR should be changed to support mixed hosting behaviors only when creating a new window which needs to support those behaviors. Once that window is created, the hosting behavior should be switched back to its default value. /// /// This API is used to change the thread's DPI_HOSTING_BEHAVIOR from its default value. This is only necessary if your app needs to host child windows from plugins and third-party components that do not support per-monitor-aware context. This is most likely to occur if you are updating complex applications to support per-monitor DPI_AWARENESS_CONTEXT behaviors. /// /// Enabling mixed hosting behavior will not automatically adjust the thread's DPI_AWARENESS_CONTEXT to be compatible with legacy content. The thread's awareness context must still be manually changed before new windows are created to host such content. /// /// DpiHostingBehavior /// previous DpiHostingBehavior [DllImport("User32.dll")] private static extern DpiHostingBehavior SetThreadDpiHostingBehavior(DpiHostingBehavior dpiHostingBehavior); /// ///Retrieves the DPI_HOSTING_BEHAVIOR from the current thread. /// /// DpiHostingBehavior [DllImport("User32.dll")] private static extern DpiHostingBehavior GetThreadDpiHostingBehavior(); /// /// Overrides the default per-monitor DPI scaling behavior of a child window in a dialog. /// This function returns TRUE if the operation was successful, and FALSE otherwise. To get extended error information, call GetLastError. /// /// Possible errors are ERROR_INVALID_HANDLE if passed an invalid HWND, and ERROR_ACCESS_DENIED if the windows belongs to another process. /// /// The behaviors are specified as values from the DIALOG_CONTROL_DPI_CHANGE_BEHAVIORS enum. This function follows the typical two-parameter approach to setting flags, where a mask specifies the subset of the flags to be changed. /// /// It is valid to set these behaviors on any window. It does not matter if the window is currently a child of a dialog at the point in time that SetDialogControlDpiChangeBehavior is called. The behaviors are retained and will take effect only when the window is an immediate child of a dialog that has per-monitor DPI scaling enabled. /// /// This API influences individual controls within dialogs. The dialog-wide per-monitor DPI scaling behavior is controlled by SetDialogDpiChangeBehavior. /// /// IntPtr A handle for the window whose behavior will be modified. /// DialogScalingBehaviors A mask specifying the subset of flags to be changed. /// DialogScalingBehaviors The desired value to be set for the specified subset of flags. /// bool [DllImport("User32.dll")] private static extern bool SetDialogControlDpiChangeBehavior(IntPtr hWnd, DialogScalingBehaviors mask, DialogScalingBehaviors values); /// /// Retrieves and per-monitor DPI scaling behavior overrides of a child window in a dialog. /// The flags set on the given window. If passed an invalid handle, this function will return zero, and set its last error to ERROR_INVALID_HANDLE. /// /// IntPtr A handle for the window whose behavior will be modified. /// DialogScalingBehaviors [DllImport("User32.dll")] private static extern DialogScalingBehaviors GetDialogControlDpiChangeBehavior(IntPtr hWnd); } }