0

By using Screen (Screen.AllScreens for multiscreens) class, the basic information of screens can be derived easily. However, it seems hard to get DPI or monitor scale information for each screen in multi-monitor setting.

I've found some solutions for apps with 'DPI aware' in How to get Windows Display settings?

However, as written above, they mostly require either the running app as 'DPI aware' or a single screen environment (that any Form or Control must be located in the desired screen to calculate DPI value).

I just want to get the 'scale value (100%, 125%.. in Display Setting > Scale and Layout)' (it is okay not to be DPI) for each monitor. The important thing is that the app is not declared as 'DPI aware' and it won't be. It's a WinForm legacy program.

Is it possible to do it in that setting?

Thank you in advance.

klados
  • 706
  • 11
  • 33
  • 3
    You're literally describing wanting your application to be aware of DPI, but you don't want to declare it DPI Aware. Is there a *solid technical reason* for not making the obvious change? – Damien_The_Unbeliever Mar 08 '21 at 10:03
  • [Enumeration and Display Control](https://learn.microsoft.com/en-us/windows/win32/gdi/enumeration-and-display-control) -- [Multiple Monitor System Metrics](https://learn.microsoft.com/en-us/windows/win32/gdi/multiple-monitor-system-metrics) -- [Using SetWindowPos with multiple monitors](https://stackoverflow.com/a/53026765/7444103) – Jimi Mar 08 '21 at 10:09
  • @Damien_The_Unbeliever My application needs to calculate a `Form` location according to DPI(or Scale) for arranging through multi-screens. However, declaring it as DPI-aware makes both `WinForm Controls` and other 3rd-party ones in the app quite messy, so that I don't plan to modify the bunch of whole codes and UIs. Just want to know the raw value of DPI or Scale setting of screens. The codes are already written, only the scale number is needed parameter. – klados Mar 08 '21 at 10:12
  • 2
    If you want to keep your app Dpi-UnAware, use the virtualized measures you get from those methods. The System scales your Forms for you. You don't really need to know the scale factor of a Monitor to move your Forms to a different Screen. Different story if you have a DpiAware app and you want to scale it to adapt its layout to different Screen sizes and Font scaling (which is the harder and messier part in WinForms, IMO, since there's still lack of support for this). – Jimi Mar 08 '21 at 10:21
  • @Jimi Thanks. I've just tried `EnumDisplayMonitors` user32.dll API and it appears to show the same result as just using `Screen.AllScreens(0).Bounds`, which is not desired. I understand worth making DPI-aware, and I know that 'System will do it for you'. I've tried once and turned out that definitely, the System does not do *all* of them for me. (Mixed with native WinForm controls and 3rd-party ones mingled together to make the worst layout, plus locating a `Form` does not work as intended). – klados Mar 08 '21 at 10:53
  • The application has to calculate with the scale number (not the Windows automatically adjusted for me) to locate a certain `Form` precisely. The scale number (1.25, 125%, 1280/1024 or 120/96) is the only parameter missing, the rest of the part is already coded. – klados Mar 08 '21 at 10:53
  • You should probably use UI Automation for this. Anyway, see [GetDeviceCaps](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getdevicecaps). To understand better what the notes there are telling you, read also the notes I posted [here](https://stackoverflow.com/a/53026765/7444103), about the VirtualScreen layout and Monitor identifiers. – Jimi Mar 08 '21 at 11:03
  • @Jimi Thanks. Your note is a really comprehensive and nice one, but it says "If the application is not DPIAware ...All measures will be uniformed to a default 96 Dpi". Anyway, the API that I have to use with a scale factor is `SHAppBarMessage`. It requires non-adjusted Top and Left in pixel. For example, if just putting Screen.AllScreens(1).Bounds values to `SHAppBarMessage` in 125% scaled monitor, the AppBar fail to be located properly. I will read GetDeviceCaps. – klados Mar 08 '21 at 11:13
  • @smoorke did give me the solution. – klados Feb 15 '23 at 02:37

1 Answers1

1

Note: the signature for DwmGetWindowAttribute in this snippet is incorrect for uses other than DWMWA_EXTENDED_FRAME_BOUNDS or anything that uses something else than a RECT for the pvAttribute.

Public Declare Function DwmGetWindowAttribute Lib "dwmapi" (ByVal hwnd As IntPtr, ByVal dwAttribute As Integer, ByRef pvAttribute As RECT, ByVal cbAttribute As Integer) As Integer
Const DWMWA_EXTENDED_FRAME_BOUNDS As Integer = 9

<System.Runtime.InteropServices.DllImport("user32.dll")>
Public Function GetWindowRect(ByVal hWnd As IntPtr, ByRef lpRect As RECT) As Boolean : End Function

<System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)>
Public Structure RECT
    Public left, top, right, bottom As Integer
End Structure

Current Form:

Public Function GetScalingPercent() As Integer
    Dim rcFrame As RECT
    DwmGetWindowAttribute(Me.Handle, DWMWA_EXTENDED_FRAME_BOUNDS, rcFrame, System.Runtime.InteropServices.Marshal.SizeOf(rcFrame))
    Dim rcWind As RECT
    GetWindowRect(Me.Handle, rcWind)
    Return Int((rcFrame.right - rcFrame.left) / (rcWind.right - rcWind.left) * 100 / 25) * 25 + If(Me.FormBorderStyle = FormBorderStyle.None, 0, 25)
End Function

Your window must be shown on the screen you wish to get the scaling from. So don't use this in the Form.Load Event

Extension to Screen Object:

<System.Runtime.CompilerServices.Extension()>
Public Function ScalingPercent(scrn As Screen) As Integer
    Dim grab As New InactiveForm With {
        .FormBorderStyle = FormBorderStyle.None,
        .TransparencyKey = Color.Red,
        .BackColor = Color.Red,
        .ShowInTaskbar = False,
        .StartPosition = FormStartPosition.Manual,
        .Location = scrn.Bounds.Location
        }
    AddHandler grab.Shown, Sub()
                                  grab.Location += New Point(1, 1) 'need to update the location so the frame changes
                                  Dim rcFrame As RECT
                                  DwmGetWindowAttribute(grab.Handle, DWMWA_EXTENDED_FRAME_BOUNDS, rcFrame, System.Runtime.InteropServices.Marshal.SizeOf(rcFrame))
                                  Dim rcWind As RECT
                                  GetWindowRect(grab.Handle, rcWind)
                                  grab.Tag = Int((rcFrame.right - rcFrame.left) / (rcWind.right - rcWind.left) * 100 / 25) * 25
                                  grab.Close()
                           End Sub
    grab.ShowDialog()
    Return grab.Tag
End Function

Private Class InactiveForm : Inherits Form
    Protected Overloads Overrides ReadOnly Property ShowWithoutActivation() As Boolean
        Get
            Return True
        End Get
    End Property
End Class

Example usage:

For Each scrn As Screen In Screen.AllScreens
    Debug.Print(scrn.ScalingPercent)
Next
smoorke
  • 49
  • 4
  • Thanks. But, the result value seems incorrect. On a 100% scaled monitor, it shows 75. It gives 100 when the monitor is set to 125% scaled. I'm not sure if it is consistent for any other devices so I can adjust it arbitrarily. – klados Feb 08 '23 at 14:22
  • 1
    @klados i've updated my answer. the discrepancy was due to the form having a border or not – smoorke Feb 13 '23 at 23:37
  • Awesome! It works. Simple and precise. – klados Feb 15 '23 at 02:39
  • @klados updated my answer once again to address the (Form or Control must be located in the desired screen to calculate DPI value) issue you raised – smoorke Feb 16 '23 at 08:07