Search Unity

Resolved [SOLVED, kinda] (user32.dll) Obtaining monitor's DPI-independent maximised screen size.

Discussion in 'Windows' started by SEYLOR_, Jun 30, 2020.

  1. SEYLOR_

    SEYLOR_

    Joined:
    Oct 4, 2019
    Posts:
    2
    Hello all!
    I'm having an absolutely fantastic, totally not gut-wrenching time working with system APIs.

    For this project, I have implemented transparent windows to create a rounded border effect. To go along side this, I have added a custom ui titlebar. At no point do I want the window to be fullscreen as, despite it being a game, I want the user to feel like they're using an application of some description. This is all done to deliver a minimalist UX.
    I wanted to write my own behaviour for "maximising" and collapsing the window. However, in order to do this, I need to tell unity the new window resolution and re-apply it after unity has finished updating it. This aspect of the whole process works: the window "maximises" by resizing through
    SetWindowPos
    just fine. However, whenever I ask user32.dll to get me the default maximised window size, it returns dimensions larger than the monitor the window is currently sitting on.

    Code (CSharp):
    1. target = new Vector2(
    2.   (float)GetSystemMetrics(61), //61 == SM_CXMAXIMIZED
    3.   (float)GetSystemMetrics(62)  //62 == SM_CXMAXIMIZED
    4. );
    5. //Outputs - target.x: 1938, target.y: 1048
    I use a laptop with DPI scaling set to 125%, so I wondered if that had anything to do with it. A fairly recent Windows update added GetSystemMetricsForDpi so I tried toying around with that.

    Code (CSharp):
    1. uint dpiX = 0, dpiY = 0;
    2. GetDpiForMonitor(MonitorFromWindow(GetActiveWindow(), 0), DPITYPE.Effective, out dpiX, out dpiY);
    3.  
    4. target = new Vector2(
    5.   GetSystemMetricsForDpi(61, dpiX),
    6.   GetSystemMetricsForDpi(62, dpiY)
    7. );
    8. //Outputs - dpiX: 120, dpiY: 120, target.x: 1938, target.y: 1048
    Clearly I'm getting the custom DPI fine (or at least I think I am) but
    GetSystemMetricsForDpi
    doesn't appear to have changed anything.

    I'm not really sure what to do from here. One problem I think it could be might have something to do with the fact that even when the window is "maximised", Windows still views the window as minimised. Regardless of this, I need to tell Unity the new maximised window size so that it renders everything to the correct size, and it doesn't help explain why
    GetSystemMetrics
    is returning larger dimensions.

    If it helps, my relevant(?) system specs are as follows:
    • Primary 1920x1080 monitor displays at 125% dpi scaling.
    • Secondary 1920x1080 monitor at 100% dpi scaling.
    • Nvidia RTX2060
    • Unity 2019.2.0f1 Pro - Education
    • Windows 10 Home (10.0.18362 Build 18362)
    This is just a little summer project to experiment with the information on UX I learned last semester at uni - really not a matter of urgency at all. Just curious if anyone else has had this issue or could help point me in the right direction.

    Here's the full TransparentWindow.cs script. (It's based on a hodge-podge of code on an earlier thread on transparent windows as well as my maximisation code.)

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Runtime.InteropServices;
    4. using UnityEngine;
    5.  
    6. public class TransparentWindow : MonoBehaviour {
    7.  
    8. #pragma warning disable
    9.     [SerializeField]
    10.     private Material m_Material;
    11.  
    12.     private struct MARGINS {
    13. #pragma warning disable
    14.         public int cxLeftWidth;
    15. #pragma warning disable
    16.         public int cxRightWidth;
    17. #pragma warning disable
    18.         public int cyTopHeight;
    19. #pragma warning disable
    20.         public int cyBottomHeight;
    21.     }
    22.  
    23.     public enum DPITYPE {
    24.         Effective = 0,
    25.         Angular = 1,
    26.         Raw = 2
    27.     }
    28.  
    29.     [DllImport("user32.dll")]
    30.     private static extern IntPtr GetActiveWindow();
    31.  
    32.     [DllImport("user32.dll")]
    33.     private static extern int GetSystemMetricsForDpi(int nIndex, uint dpi);
    34.  
    35.     [DllImport("user32.dll")]
    36.     private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
    37.  
    38.     [DllImport("Shcore.dll")]
    39.     private static extern IntPtr GetDpiForMonitor([In] IntPtr hmonitor, [In] DPITYPE dpiType, [Out] out uint dpiX, [Out] out uint dpiY);
    40.  
    41.     [DllImport("user32.dll")]
    42.     private static extern int SetWindowLong(IntPtr hWnd, int nIndex, uint dwNewLong);
    43.  
    44.     [DllImport("user32.dll", EntryPoint = "SetWindowPos")]
    45.     private static extern int SetWindowPos(IntPtr hwnd, int hwndInsertAfter, int x, int y, int cx, int cy, int uFlags);
    46.  
    47.     [DllImport("user32.dll")]
    48.     private static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint colorref, byte bAlpha, uint dwFlags);
    49.  
    50.     [DllImport("Dwmapi.dll")]
    51.     private static extern uint DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS margins);
    52.  
    53.     [DllImport("user32.dll")]
    54.     private static extern bool ShowWindow(IntPtr hwnd, int nCmdShow);
    55.  
    56.     const int GWL_STYLE = -16;
    57.     const uint WS_POPUP = 0x80000000;
    58.     const uint WS_VISIBLE = 0x10000000;
    59.     const int HWND_TOPMOST = -1;
    60.  
    61.     int fWidth;
    62.     int fHeight;
    63.     private Vector2 previousDimensions;
    64.     private bool isMaximised;
    65.  
    66.     void Start() {
    67.         previousDimensions = new Vector2(1280, 720);
    68.         isMaximised = false;
    69.         StartCoroutine(_ChangeResolution(true));
    70.     }
    71.  
    72.  
    73.     IEnumerator _ChangeResolution(bool ignoreIsMaximised) {
    74.         Vector2 target = Vector2.zero;
    75.  
    76.         if (!isMaximised && !ignoreIsMaximised) {
    77.             previousDimensions = new Vector2(Screen.width, Screen.height);
    78.  
    79.             uint dpiX = 0, dpiY = 0;
    80.             GetDpiForMonitor(MonitorFromWindow(GetActiveWindow(), 0), DPITYPE.Effective, out dpiX, out dpiY);
    81.  
    82.             target = new Vector2(
    83.                 GetSystemMetricsForDpi(61, dpiX),
    84.                 GetSystemMetricsForDpi(62, dpiY));
    85.  
    86.             Debug.Log("DPIX: " + dpiX + ", DPIY: " + dpiY + ", TARGETX: " + target.x + ", TARGETY: " + target.y);
    87.         }
    88.         else {
    89.             target = previousDimensions;
    90.         }
    91.  
    92.         if (!ignoreIsMaximised) {
    93.             isMaximised = !isMaximised;
    94.         }
    95.  
    96.  
    97.         Screen.SetResolution((int)target.x, (int)target.y, false);
    98.         yield return new WaitUntil(() => Screen.width == target.x && Screen.height == target.y);
    99.         UpdateTransparentWindow();
    100.     }
    101.  
    102.     public void ToggleMaximised() {
    103. //#if !UNITY_EDITOR   // You really don't want to enable this in the editor..
    104.         StartCoroutine(_ChangeResolution(false));
    105. //#endif
    106.     }
    107.  
    108.     private void UpdateTransparentWindow() {
    109. #if !UNITY_EDITOR   // You really don't want to enable this in the editor..
    110.  
    111.         var margins = new MARGINS() { cxLeftWidth = -1 };
    112.         var hwnd = GetActiveWindow();
    113.  
    114.         fWidth = Screen.width;
    115.         fHeight = Screen.height;
    116.  
    117.         SetWindowLong(hwnd, GWL_STYLE, WS_POPUP | WS_VISIBLE);
    118.  
    119.         if (isMaximised) {
    120.             SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, fWidth, fHeight, 32 | 64);
    121.         }
    122.         else {
    123.             SetWindowPos(hwnd, HWND_TOPMOST, fWidth / 4, fHeight / 4, fWidth, fHeight, 32 | 64);
    124.         }
    125.  
    126.         SetLayeredWindowAttributes(hwnd, 0x0000ff00, (byte)255, 0x00000001);
    127.  
    128.         DwmExtendFrameIntoClientArea(hwnd, ref margins);
    129. #endif
    130.     }
    131.  
    132.     public void MinimiseWindow() {
    133. #if !UNITY_EDITOR
    134.         ShowWindow(GetActiveWindow(), 2);
    135. #endif
    136.     }
    137.  
    138.     void OnRenderImage(RenderTexture from, RenderTexture to) {
    139.         Graphics.Blit(from, to, m_Material);
    140.     }
    141. }

    If anyone can help or offer input of any kind, it would be greatly appreciated! :)
     
  2. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
  3. SEYLOR_

    SEYLOR_

    Joined:
    Oct 4, 2019
    Posts:
    2
    I'm not too sure if
    Screen.currentResolution
    is what I needed, partially because I've messed around with removing the titlebar and setting the window to a pop-up window. I was specifically needing the rect of a maximised window. I've since discovered that this is called the working area in the Windows API. I have managed to access this through
    System.Windows.Forms
    . I know it's a bit dodgy to be using the Windows Forms API alongside Unity, but
    • I used the Mono versions of the API.
    • I only plan on deploying to a Windows executable.
    • I'm only reading values from the API, not setting anything.
    I don't have the code up right now, but I believe it was through the
    System.Windows.Forms.Screen.GetWorkingArea(System.Drawing.Point screenPoint)
    function.
    (MSDN | Screen.GetWorkingArea Method)

    With regards to primary and secondary monitor switching, I'll need to do a bit more testing for this. However, I believe I can receive a screen point from a window, which can be supplied to back into the working area function.

    Still curious as to why
    GetSystemMetricsForDpi
    and
    GetSystemMetrics
    didn't work, especially considering I was testing solely on my primary monitor and I was supplying the DPI for the
    GetSystemMetricsForDpi
    function. I think there's a suggestion that the system metrics adheres to desktop space as opposed to screen space, but I'm not sure.
    (MSDN | GetSystemMetrics function)
    (MSDN | GetSystemMetricsForDpi function)

    Either way, the Forms function works. I'll just need to be careful with how the .dll is utilised. I've put it in the Assets/Plugins folder. (I think I found it from the Mono library folder installed with Unity.) Haven't tested how it's going to play if I release an x86_64 vs an x86 build, but it does indeed build and works perfectly for what I need it for. :)
     
  4. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    I think GetSystemMetrics works, it just gives you different data than you expect. It likely takes into account Windows themes, borders, title bar and the task bar.