[Windows] DPI awareness Per Monitor v2

Share your advanced PureBasic knowledge/code with the community.
breeze4me
Enthusiast
Enthusiast
Posts: 657
Joined: Thu Mar 09, 2006 9:24 am
Location: S. Kor

[Windows] DPI awareness Per Monitor v2

Post by breeze4me »

https://learn.microsoft.com/en-us/windo ... -a-process

The following examples for DPI awareness PM v2 demonstrate how to replace the contents of the manifest file and how to use the native commands of PB.
(Not tested in DPI awareness PM v1 mode.)

Unfortunately, this method is thread-unsafe. :(

1. Compile the following code to generate the new "porc.exe" file.
2. Rename the "porc.exe" file in the "Compilers" folder to "porc-o.exe".
3. Copy the newly created "porc.exe" file to the "Compilers" folder.

Edit:
Fixed an issue where reading would stop when a blank line appeared.

Code: Select all

DPI_PMv2_2$ = ~"  <asmv3:application xmlns:asmv3=\"urn:schemas-microsoft-com:asm.v3\">\n" +
              ~"    <asmv3:windowsSettings>\n" +
              ~"      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true/pm</dpiAware>\n" +
              ~"      <dpiAwareness xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">PerMonitorV2, PerMonitor, System</dpiAwareness>\n" +
              ~"    </asmv3:windowsSettings>\n" +
              ~"  </asmv3:application>\n"

Path$ = GetPathPart(ProgramParameter(0))
ManifestFile$ = Path$ + "Manifest"

For i = 0 To CountProgramParameters() - 1
  Param$ + ProgramParameter(i) + " "
Next

Param$ = Trim(Param$)

If OpenFile(0, ManifestFile$)
  While Eof(0) = 0
    Line$ = ReadString(0, #PB_UTF8)
    If Line$
      If FindString(Line$, "<dpiAware>true</dpiAware>", 20)
        NewManifest$ + DPI_PMv2_2$
      Else
        NewManifest$ + Line$ + #LF$
      EndIf
    EndIf
  Wend
  CloseFile(0)
  
  NewManifest$ = Trim(RTrim(NewManifest$, #LF$))
  If NewManifest$
    DeleteFile(ManifestFile$)
    If CreateFile(0, ManifestFile$)
      WriteString(0, NewManifest$, #PB_UTF8)
      CloseFile(0)
    EndIf
  EndIf
EndIf

; MessageRequester("" + CountProgramParameters(), Param$)
RunProgram("porc-o.exe", Param$, "", #PB_Program_Hide | #PB_Program_Wait)
We are now ready to apply DPI awareness PM v2.

Run the code below to verify that it works correctly when the DPI is changed.
Warning: The GetFontDPIAddr function has only been tested in PB v6.30, so problems may occur in other PB versions.

Code: Select all


CompilerIf Not #PB_Compiler_DPIAware
  CompilerError "Turn ON the DPI aware option."
CompilerEndIf

Import ""
  PB_Desktop_DPIX.l
  PB_Desktop_DPIY.l
  PB_Desktop_ResolutionX.d
  PB_Desktop_ResolutionY.d
  
  PB_Font_Objects.i
EndImport


;- OS Constants.
#STATUS_SUCCESS = 0

; Windows 8.1+
#WM_DPICHANGED = $02E0

; Windows 10 1703+
#WM_GETDPISCALEDSIZE = $02E4

; MONITOR_DPI_TYPE
#MDT_EFFECTIVE_DPI = 0
#MDT_ANGULAR_DPI = 1
#MDT_RAW_DPI = 2
#MDT_DEFAULT = #MDT_EFFECTIVE_DPI




;- App Constants.

#_WIN32_WINNT_VISTA    = $0600
#_WIN32_WINNT_WIN7     = $0601
#_WIN32_WINNT_WIN8     = $0602
;#_WIN32_WINNT_WINBLUE  = $0603 ; 8.1
#_WIN32_WINNT_WIN81    = $0603
#_WIN32_WINNT_WIN10    = $0A00

#WIN10_BUILD_1607 = $3839 ; 14393 , 1607
#WIN10_BUILD_1703 = $3AD7 ; 15063 , 1703    ;Support for per-monitor DPI awareness v2.
#WIN10_BUILD_1709 = $3FAB ; 16299 , 1709
#WIN10_BUILD_1803 = $42EE ; 17134 , 1803

Enumeration 
  #Lib_User32
  #Lib_Shcore
EndEnumeration


;- OS Structures.

Structure NONCLIENTMETRICS_vista
  cbSize.l
  iBorderWidth.l
  iScrollWidth.l
  iScrollHeight.l
  iCaptionWidth.l
  iCaptionHeight.l
  lfCaptionFont.LOGFONT
  iSMCaptionWidth.l
  iSMCaptionHeight.l
  lfSMCaptionFont.LOGFONT
  iMenuWidth.l
  iMenuHeight.l
  lfMenuFont.LOGFONT
  lfStatusFont.LOGFONT
  lfMessageFont.LOGFONT
  iPaddedBorderWidth.l
EndStructure


;- OS APIs (Global var.)

; Windows 8.1+
Prototype.l ptGetDpiForMonitor(hmonitor, dpiType.l, *dpiX, *dpiY)
Global GetDpiForMonitor__.ptGetDpiForMonitor

; Windows 10 1607
Prototype.l ptEnableNonClientDpiScaling(hwnd)
Global EnableNonClientDpiScaling__.ptEnableNonClientDpiScaling

; Windows 10 1607+
Prototype.l ptGetDpiForWindow(hwnd)
Global GetDpiForWindow__.ptGetDpiForWindow

; Windows 10 1607+
Prototype.l ptSystemParametersInfoForDpi(uiAction.l, uiParam.l, *pvParam, fWinIni.l, dpi.l)
Global SystemParametersInfoForDpi__.ptSystemParametersInfoForDpi



;- App Global Variables.
Global g_OSVersion.OSVERSIONINFOEX
Global *g_FontDPI.Long      ; Memory address for DPI value of PB default font.



;- App Functions.

Declare.l GetDpiForWindow(hWnd)

Procedure GetOSVersion(*OSVersion.OSVERSIONINFOEX)
  Protected Result, LibNtDll = OpenLibrary(#PB_Any, "Ntdll.dll")
  If LibNtDll
    If *OSVersion
      *OSVersion\dwOSVersionInfoSize = SizeOf(OSVERSIONINFOEX)
      If CallFunction(LibNtDll, "RtlGetVersion", *OSVersion) = #STATUS_SUCCESS
        Result = 1
      EndIf
    EndIf
    CloseLibrary(LibNtDll)
  EndIf
  ProcedureReturn Result
EndProcedure

Procedure LoadOSFunctions()
  
  If OpenLibrary(#Lib_Shcore, "Shcore.dll")
    ; Windows 8.1+
    GetDpiForMonitor__ = GetFunction(#Lib_Shcore, "GetDpiForMonitor")
    
  EndIf
  
  If OpenLibrary(#Lib_User32, "User32.dll")
    
    ; Windows 10 1607
    EnableNonClientDpiScaling__ = GetFunction(#Lib_User32, "EnableNonClientDpiScaling")
    
    ; Windows 10 1607+
    GetDpiForWindow__ = GetFunction(#Lib_User32, "GetDpiForWindow")
    
    ; Windows 10 1607+
    SystemParametersInfoForDpi__ = GetFunction(#Lib_User32, "SystemParametersInfoForDpi")
    
  EndIf
  
  ; This function must always be callable without failure.
  If GetDpiForWindow__ = 0
    GetDpiForWindow__ = @GetDpiForWindow()
    Debug "GetDpiForWindow__ : Using the app's function."
  EndIf
  
EndProcedure

Procedure.l GetSystemDPI()
  Protected hDC = CreateDC_("DISPLAY", 0, 0, 0)
  Protected DpiX.l
  If hDC
    DpiX = GetDeviceCaps_(hDC, #LOGPIXELSX)
    DeleteDC_(hDC)
  EndIf
  ProcedureReturn DpiX
EndProcedure

Procedure.l GetDpiForWindow(hWnd)
  Protected hMonitor = MonitorFromWindow_(hWnd, #MONITOR_DEFAULTTONEAREST)
  Protected DpiX.l, DpiY.l
  
  If hMonitor
    ; Windows 8.1+
    If GetDpiForMonitor__
      If GetDpiForMonitor__(hMonitor, #MDT_EFFECTIVE_DPI, @DpiX, @DpiY) = #S_OK
        ProcedureReturn DpiX
      EndIf
    EndIf
  EndIf
  
  DpiX = GetSystemDPI()
  If DpiX = 0 : DpiX = 96 : EndIf
  
  ProcedureReturn DpiX
EndProcedure

Procedure LoadSystemDefaultFont(dpi.l)
  Protected Font, Height.l, SystemDPI.l, Prev_FontDPI.l, ncm.NONCLIENTMETRICS_vista
  
  If SystemParametersInfoForDpi__
    ncm\cbSize = SizeOf(NONCLIENTMETRICS_vista)
    If SystemParametersInfoForDpi__(#SPI_GETNONCLIENTMETRICS, SizeOf(NONCLIENTMETRICS_vista), @ncm, 0, dpi)
      Debug #PB_Compiler_Procedure + " - OK: SystemParametersInfoForDpi"
    EndIf
  Else
    SystemDPI = GetSystemDPI()
    ncm\cbSize = SizeOf(NONCLIENTMETRICS)
    If SystemParametersInfo_(#SPI_GETNONCLIENTMETRICS, SizeOf(NONCLIENTMETRICS), @ncm, 0)
      Debug #PB_Compiler_Procedure + " - OK: SystemParametersInfo"
      ncm\lfMessageFont\lfHeight = ncm\lfMessageFont\lfHeight * dpi / SystemDPI
    EndIf
  EndIf
  
  If ncm\lfMessageFont\lfHeight < 0
    Height = MulDiv_(-ncm\lfMessageFont\lfHeight, 72, dpi)
  Else
    Height = ncm\lfMessageFont\lfHeight
  EndIf
  
  Debug #PB_Compiler_Procedure + " - Font height: " + Height + " (DPI: " + dpi + " )"
  
  If *g_FontDPI
    Prev_FontDPI = *g_FontDPI\l
    *g_FontDPI\l = dpi
  EndIf
  
  Font = LoadFont(#PB_Any, PeekS(@ncm\lfMessageFont\lfFaceName[0]), Height)
  
  If *g_FontDPI
    *g_FontDPI\l = Prev_FontDPI
  EndIf
  
  ProcedureReturn Font
EndProcedure

Procedure GetFontDPIAddr()
  If Defined(PB_Font_Objects, #PB_Variable)
    ProcedureReturn @PB_Font_Objects + SizeOf(Integer)
  EndIf
  ProcedureReturn 0
EndProcedure

Procedure WinCallback_DPI(hWnd, uMsg, wParam, lParam)
  If uMsg = #WM_NCCREATE
    If GetAncestor_(hWnd, #GA_ROOT) = hWnd
      If EnableNonClientDpiScaling__
        EnableNonClientDpiScaling__(hWnd)
      EndIf
    EndIf
  EndIf
  ProcedureReturn #PB_ProcessPureBasicEvents
EndProcedure

Procedure StartDPIAction(dpi.l)
  Static Prev_PB_Desktop_DPIX.l, Prev_PB_Desktop_DPIY.l
  Static Prev_PB_Desktop_ResolutionX.d, Prev_PB_Desktop_ResolutionY.d
  Static Prev_FontDPI.l
  
  If dpi < 0
    ; for StopDPIAction()
    
    If Prev_PB_Desktop_DPIX > 0
      PB_Desktop_DPIX = Prev_PB_Desktop_DPIX
    Else
      PB_Desktop_DPIX = GetSystemDPI()
    EndIf
    
    If Prev_PB_Desktop_DPIY > 0
      PB_Desktop_DPIY = Prev_PB_Desktop_DPIY
    Else
      ;PB_Desktop_DPIY = GetSystemDPI()
      PB_Desktop_DPIY = PB_Desktop_DPIX
    EndIf
    
    If *g_FontDPI
      If Prev_FontDPI > 0
        *g_FontDPI\l = Prev_FontDPI
      Else
        *g_FontDPI\l = PB_Desktop_DPIX
      EndIf
    EndIf
    
    Prev_PB_Desktop_DPIX = 0
    Prev_PB_Desktop_DPIY = 0
    Prev_PB_Desktop_ResolutionX = 0
    Prev_PB_Desktop_ResolutionY = 0
    Prev_FontDPI = 0
    
  Else
    
    If dpi = 0
      dpi = GetSystemDPI()
    EndIf
    
    Prev_PB_Desktop_DPIX = PB_Desktop_DPIX
    Prev_PB_Desktop_DPIY = PB_Desktop_DPIY
    
    Prev_PB_Desktop_ResolutionX = PB_Desktop_ResolutionX
    Prev_PB_Desktop_ResolutionY = PB_Desktop_ResolutionY
    
    PB_Desktop_DPIX = dpi
    PB_Desktop_DPIY = dpi
    
    PB_Desktop_ResolutionX = dpi / 96
    PB_Desktop_ResolutionY = dpi / 96
    
    If *g_FontDPI
      Prev_FontDPI = *g_FontDPI\l
      *g_FontDPI\l = dpi
    EndIf
    
  EndIf
EndProcedure

Macro StopDPIAction()
  StartDPIAction(-1)
EndMacro




;- App startup.

LoadOSFunctions()

GetOSVersion(@g_OSVersion)

*g_FontDPI = GetFontDPIAddr()

; To receive the #WM_NCCREATE message, the callback must be set before any window is created.
If g_OSVersion\dwMajorVersion = 10 And (g_OSVersion\dwBuildNumber >= #WIN10_BUILD_1607 And g_OSVersion\dwBuildNumber < #WIN10_BUILD_1703)
  SetWindowCallback(@WinCallback_DPI())
EndIf


;- GUI

#WindowWidth  = 390
#WindowHeight = 350

Procedure ResizeChildren(dpi.l)
  
  Static PrevFont
  Protected hFont
  Protected Font = LoadSystemDefaultFont(dpi)
  
  If Font = 0
    ProcedureReturn 0
  EndIf
  
  hFont = FontID(Font)
  
  StartDPIAction(dpi)
  
  Top = 10
  GadgetHeight = 24
  
  ResizeGadget(40, 10, Top, 370, 290)
  SetGadgetFont(40, hFont)
  
  Top+20
  
  ResizeGadget(0,  20, Top, 200, GadgetHeight)
  ResizeGadget(1, 223, Top,  72, GadgetHeight)
  ResizeGadget(2, 295, Top,  72, GadgetHeight)
  
  SetGadgetFont(0, hFont)
  SetGadgetFont(1, hFont)
  SetGadgetFont(2, hFont)
  
  Top+35
  
  ResizeGadget(3, 20, Top, #WindowWidth-50, #WindowHeight-Top-60)
  SetGadgetFont(3, hFont)
  
  ResizeGadget(4, 6, 10, 230, 148)
  SetGadgetFont(4, hFont)
  
  ResizeGadget(5,  250, 10, 80, GadgetHeight)
  ResizeGadget(6,  250, 38, 80, GadgetHeight)
  ResizeGadget(7,  250, 66, 80, GadgetHeight)
  
  SetGadgetFont(5, hFont)
  SetGadgetFont(6, hFont)
  SetGadgetFont(7, hFont)
  
  ResizeGadget(17, 10, 168, 310, 25)
  
  Top = 10
  
  ResizeGadget(10, 10, Top, 250, GadgetHeight)
  SetGadgetFont(10, hFont)
  
  Top+30
  
  ResizeGadget(11, 10, Top, 250, GadgetHeight)
  SetGadgetFont(11, hFont)
  
  Top+30
  
  ResizeGadget(12, 10, Top, 250, 21)
  SetGadgetFont(12, hFont)
  
  Top+30
  
  ResizeGadget(13, 10, Top, 80, GadgetHeight)
  SetGadgetFont(13, hFont)
  
  Top+20
  
  ResizeGadget(14, 10, Top, 80, GadgetHeight)
  SetGadgetFont(14, hFont)
  
  Top+20
  
  ResizeGadget(15, 10, Top, 80, GadgetHeight)
  SetGadgetFont(15, hFont)
  
  ResizeGadget(16, 150, Top, 80, GadgetHeight)
  SetGadgetFont(16, hFont)
  
  ResizeGadget(9, 10, #WindowHeight-30, 250, 24)
  SetGadgetFont(9, hFont)
  
  ResizeGadget(8, #WindowWidth-100, #WindowHeight-36, 80, 24)
  SetGadgetFont(8, hFont)
  
  StopDPIAction()
  
  If PrevFont And IsFont(PrevFont)
    FreeFont(PrevFont)
  EndIf
  
  PrevFont = Font
  
EndProcedure


Procedure WinCallback(hWnd, uMsg, wParam, lParam)
  
  If uMsg = #WM_EXITSIZEMOVE
    Debug "WM_EXITSIZEMOVE"
    
    StartDPIAction(GetDpiForWindow__(hWnd))
    Debug "Window W,H = " + WindowWidth(0) + " , " + WindowHeight(0)
    StopDPIAction()
  EndIf
  
  ; Windows 10 1703+
  If uMsg = #WM_GETDPISCALEDSIZE
    
    Debug "WM_GETDPISCALEDSIZE"
    
  EndIf
  
  ; Windows 8.1+
  If uMsg = #WM_DPICHANGED
    
    Debug "WM_DPICHANGED"
    
    Protected *rt.RECT
    
    *rt = lParam
    If *rt
      With *rt
        ; Change the window size.
        SetWindowPos_(hWnd, 0, \left, \top, \right - \left, \bottom - \top, #SWP_NOZORDER | #SWP_NOACTIVATE)
      EndWith
      
      ; Change the size of all child gadgets.
      ResizeChildren(wParam & $FFFF)
    EndIf
    
  EndIf 
  
  ProcedureReturn #PB_ProcessPureBasicEvents
EndProcedure

If OpenWindow(0, 100, 200, #WindowWidth, #WindowHeight, "PureBasic - Gadget Demonstration", #PB_Window_MinimizeGadget)
  
  FrameGadget(40, 0, 0, 0, 0, "Player...")

  StringGadget(0, 0, 0, 0, 0, "")
  ButtonGadget(1, 0, 0, 0, 0, "Play")
  ButtonGadget(2, 0, 0, 0, 0, "Stop")
  DisableGadget(2,1)
  
  GadgetToolTip(1,"Play the current song")
  
  PanelGadget(3, 0, 0, 0, 0)
    AddGadgetItem(3, 0, "MP3 PlayList")
      ListViewGadget(4, 0, 0, 0, 0)

      For k=0 To 30
        AddGadgetItem(4, -1, "Music Song #"+Str(k))
      Next

      ButtonGadget(5, 0, 0, 0, 0, "Add")
      ButtonGadget(6, 0, 0, 0, 0, "Remove")
      ButtonGadget(7, 0, 0, 0, 0, "Select")
      GadgetToolTip(7, "Select the current song")
      
      TrackBarGadget(17, 0, 0, 0, 0, 0, 100)

    AddGadgetItem(3, 1, "Options")
      Top = 10
      CheckBoxGadget(10, 0, 0, 0, 0, "Enable low-pass filter")
      CheckBoxGadget(11, 0, 0, 0, 0, "Enable visual plug-in")
      ComboBoxGadget(12, 0, 0, 0, 0)
        AddGadgetItem(12, -1, "FireWorks")
        AddGadgetItem(12, -1, "OpenGL spectrum")
        AddGadgetItem(12, -1, "Bump bass")
      SetGadgetState(12,0)
      DisableGadget(12,1)
      
      OptionGadget(13, 0, 0, 0, 0, "640*480")
      OptionGadget(14, 0, 0, 0, 0, "800*600")
      OptionGadget(15, 0, 0, 0, 0, "1024*768")
      SetGadgetState(13, 1)
      
      ButtonGadget(16, 0, 0, 0, 0, "Info")
  CloseGadgetList()

  TextGadget  (9, 0, 0, 0, 0, "PureBasic - Gadget demonstration")
  ButtonGadget(8, 0, 0, 0, 0, "Quit")

  SetGadgetState(3, 0)
  
  ; Change the size of all child gadgets.
  ResizeChildren(GetDpiForWindow__(WindowID(0)))
  SetWindowCallback(@WinCallback(), 0)
  
  Debug "Window W,H (init) = " + WindowWidth(0) + " , " + WindowHeight(0)
  
  
  Repeat
    Event = WaitWindowEvent()
    
    If Event = #PB_Event_Gadget

      Select EventGadget()
        Case 1 ; Play
          DisableGadget(2,0)  ; Enable the 'Stop' gadget
          DisableGadget(1,1)  ; Disable the 'Play' Gadget
      
        Case 2 ; Stop
          DisableGadget(1,0)  ; Enable the 'Play' gadget
          DisableGadget(2,1)  ; Disable the 'Stop' Gadget
        
        Case 4
          If EventType() = 2
            SetGadgetText(0, GetGadgetText(4)) ; Get the current item from the ListView..
          EndIf

        Case 5 ; Add
          AddGadgetItem(4, -1, "New Item Added...")

        Case 6 ; Remove
          RemoveGadgetItem(4, GetGadgetState(4)) ; Remove the current element of the ListView

        Case 7 ; Select
          SetGadgetText(0, GetGadgetText(4)) ; Get the current item from the ListView..
  
        Case 8 ; Quit...
          Event = #PB_Event_CloseWindow

        Case 11 ; Enable PlugIn..
          DisableGadget(12, 1-GetGadgetState(11))
          
        Case 16 ;
          If GetGadgetState(13) : Result$ = GetGadgetText(13) : EndIf
          If GetGadgetState(14) : Result$ = GetGadgetText(14) : EndIf
          If GetGadgetState(15) : Result$ = GetGadgetText(15) : EndIf
         
          MessageRequester("Info", "Selected screen mode: "+Result$, 0)
        
        Case 17
          SetGadgetText(0, Str(GetGadgetState(17)))
          
      EndSelect

    EndIf

  Until Event = #PB_Event_CloseWindow

EndIf
Another example.

Code: Select all

CompilerIf Not #PB_Compiler_DPIAware
  CompilerError "Turn ON the DPI aware option."
CompilerEndIf

Import ""
  PB_Desktop_DPIX.l
  PB_Desktop_DPIY.l
  PB_Desktop_ResolutionX.d
  PB_Desktop_ResolutionY.d
  
  PB_Font_Objects.i
EndImport


;- OS Constants.
#STATUS_SUCCESS = 0

; Windows 8.1+
#WM_DPICHANGED = $02E0

; Windows 10 1703+
#WM_GETDPISCALEDSIZE = $02E4

; MONITOR_DPI_TYPE
#MDT_EFFECTIVE_DPI = 0
#MDT_ANGULAR_DPI = 1
#MDT_RAW_DPI = 2
#MDT_DEFAULT = #MDT_EFFECTIVE_DPI




;- App Constants.

#_WIN32_WINNT_VISTA    = $0600
#_WIN32_WINNT_WIN7     = $0601
#_WIN32_WINNT_WIN8     = $0602
;#_WIN32_WINNT_WINBLUE  = $0603 ; 8.1
#_WIN32_WINNT_WIN81    = $0603
#_WIN32_WINNT_WIN10    = $0A00

#WIN10_BUILD_1607 = $3839 ; 14393 , 1607
#WIN10_BUILD_1703 = $3AD7 ; 15063 , 1703    ;Support for per-monitor DPI awareness v2.
#WIN10_BUILD_1709 = $3FAB ; 16299 , 1709
#WIN10_BUILD_1803 = $42EE ; 17134 , 1803

Enumeration 
  #Lib_User32
  #Lib_Shcore
EndEnumeration


;- OS Structures.

Structure NONCLIENTMETRICS_vista
  cbSize.l
  iBorderWidth.l
  iScrollWidth.l
  iScrollHeight.l
  iCaptionWidth.l
  iCaptionHeight.l
  lfCaptionFont.LOGFONT
  iSMCaptionWidth.l
  iSMCaptionHeight.l
  lfSMCaptionFont.LOGFONT
  iMenuWidth.l
  iMenuHeight.l
  lfMenuFont.LOGFONT
  lfStatusFont.LOGFONT
  lfMessageFont.LOGFONT
  iPaddedBorderWidth.l
EndStructure


;- OS APIs (Global var.)

; Windows 8.1+
Prototype.l ptGetDpiForMonitor(hmonitor, dpiType.l, *dpiX, *dpiY)
Global GetDpiForMonitor__.ptGetDpiForMonitor

; Windows 10 1607
Prototype.l ptEnableNonClientDpiScaling(hwnd)
Global EnableNonClientDpiScaling__.ptEnableNonClientDpiScaling

; Windows 10 1607+
Prototype.l ptGetDpiForWindow(hwnd)
Global GetDpiForWindow__.ptGetDpiForWindow

; Windows 10 1607+
Prototype.l ptSystemParametersInfoForDpi(uiAction.l, uiParam.l, *pvParam, fWinIni.l, dpi.l)
Global SystemParametersInfoForDpi__.ptSystemParametersInfoForDpi



;- App Global Variables.
Global g_OSVersion.OSVERSIONINFOEX
Global *g_FontDPI.Long      ; Memory address for DPI value of PB default font.



;- App Functions.

Declare.l GetDpiForWindow(hWnd)

Procedure GetOSVersion(*OSVersion.OSVERSIONINFOEX)
  Protected Result, LibNtDll = OpenLibrary(#PB_Any, "Ntdll.dll")
  If LibNtDll
    If *OSVersion
      *OSVersion\dwOSVersionInfoSize = SizeOf(OSVERSIONINFOEX)
      If CallFunction(LibNtDll, "RtlGetVersion", *OSVersion) = #STATUS_SUCCESS
        Result = 1
      EndIf
    EndIf
    CloseLibrary(LibNtDll)
  EndIf
  ProcedureReturn Result
EndProcedure

Procedure LoadOSFunctions()
  
  If OpenLibrary(#Lib_Shcore, "Shcore.dll")
    ; Windows 8.1+
    GetDpiForMonitor__ = GetFunction(#Lib_Shcore, "GetDpiForMonitor")
    
  EndIf
  
  If OpenLibrary(#Lib_User32, "User32.dll")
    
    ; Windows 10 1607
    EnableNonClientDpiScaling__ = GetFunction(#Lib_User32, "EnableNonClientDpiScaling")
    
    ; Windows 10 1607+
    GetDpiForWindow__ = GetFunction(#Lib_User32, "GetDpiForWindow")
    
    ; Windows 10 1607+
    SystemParametersInfoForDpi__ = GetFunction(#Lib_User32, "SystemParametersInfoForDpi")
    
  EndIf
  
  ; This function must always be callable without failure.
  If GetDpiForWindow__ = 0
    GetDpiForWindow__ = @GetDpiForWindow()
    Debug "GetDpiForWindow__ : Using the app's function."
  EndIf
  
EndProcedure

Procedure.l GetSystemDPI()
  Protected hDC = CreateDC_("DISPLAY", 0, 0, 0)
  Protected DpiX.l
  If hDC
    DpiX = GetDeviceCaps_(hDC, #LOGPIXELSX)
    DeleteDC_(hDC)
  EndIf
  ProcedureReturn DpiX
EndProcedure

Procedure.l GetDpiForWindow(hWnd)
  Protected hMonitor = MonitorFromWindow_(hWnd, #MONITOR_DEFAULTTONEAREST)
  Protected DpiX.l, DpiY.l
  
  If hMonitor
    ; Windows 8.1+
    If GetDpiForMonitor__
      If GetDpiForMonitor__(hMonitor, #MDT_EFFECTIVE_DPI, @DpiX, @DpiY) = #S_OK
        ProcedureReturn DpiX
      EndIf
    EndIf
  EndIf
  
  DpiX = GetSystemDPI()
  If DpiX = 0 : DpiX = 96 : EndIf
  
  ProcedureReturn DpiX
EndProcedure

Procedure LoadSystemDefaultFont(dpi.l)
  Protected Font, Height.l, SystemDPI.l, Prev_FontDPI.l, ncm.NONCLIENTMETRICS_vista
  
  If SystemParametersInfoForDpi__
    ncm\cbSize = SizeOf(NONCLIENTMETRICS_vista)
    If SystemParametersInfoForDpi__(#SPI_GETNONCLIENTMETRICS, SizeOf(NONCLIENTMETRICS_vista), @ncm, 0, dpi)
      Debug #PB_Compiler_Procedure + " - OK: SystemParametersInfoForDpi"
    EndIf
  Else
    SystemDPI = GetSystemDPI()
    ncm\cbSize = SizeOf(NONCLIENTMETRICS)
    If SystemParametersInfo_(#SPI_GETNONCLIENTMETRICS, SizeOf(NONCLIENTMETRICS), @ncm, 0)
      Debug #PB_Compiler_Procedure + " - OK: SystemParametersInfo"
      ncm\lfMessageFont\lfHeight = ncm\lfMessageFont\lfHeight * dpi / SystemDPI
    EndIf
  EndIf
  
  If ncm\lfMessageFont\lfHeight < 0
    Height = MulDiv_(-ncm\lfMessageFont\lfHeight, 72, dpi)
  Else
    Height = ncm\lfMessageFont\lfHeight
  EndIf
  
  Debug #PB_Compiler_Procedure + " - Font height: " + Height + " (DPI: " + dpi + " )"
  
  If *g_FontDPI
    Prev_FontDPI = *g_FontDPI\l
    *g_FontDPI\l = dpi
  EndIf
  
  Font = LoadFont(#PB_Any, PeekS(@ncm\lfMessageFont\lfFaceName[0]), Height)
  
  If *g_FontDPI
    *g_FontDPI\l = Prev_FontDPI
  EndIf
  
  ProcedureReturn Font
EndProcedure

Procedure GetFontDPIAddr()
  If Defined(PB_Font_Objects, #PB_Variable)
    ProcedureReturn @PB_Font_Objects + SizeOf(Integer)
  EndIf
  ProcedureReturn 0
EndProcedure

Procedure WinCallback_DPI(hWnd, uMsg, wParam, lParam)
  If uMsg = #WM_NCCREATE
    If GetAncestor_(hWnd, #GA_ROOT) = hWnd
      If EnableNonClientDpiScaling__
        EnableNonClientDpiScaling__(hWnd)
      EndIf
    EndIf
  EndIf
  ProcedureReturn #PB_ProcessPureBasicEvents
EndProcedure

Procedure StartDPIAction(dpi.l)
  Static Prev_PB_Desktop_DPIX.l, Prev_PB_Desktop_DPIY.l
  Static Prev_PB_Desktop_ResolutionX.d, Prev_PB_Desktop_ResolutionY.d
  Static Prev_FontDPI.l
  
  If dpi < 0
    ; for StopDPIAction()
    
    If Prev_PB_Desktop_DPIX > 0
      PB_Desktop_DPIX = Prev_PB_Desktop_DPIX
    Else
      PB_Desktop_DPIX = GetSystemDPI()
    EndIf
    
    If Prev_PB_Desktop_DPIY > 0
      PB_Desktop_DPIY = Prev_PB_Desktop_DPIY
    Else
      ;PB_Desktop_DPIY = GetSystemDPI()
      PB_Desktop_DPIY = PB_Desktop_DPIX
    EndIf
    
    If *g_FontDPI
      If Prev_FontDPI > 0
        *g_FontDPI\l = Prev_FontDPI
      Else
        *g_FontDPI\l = PB_Desktop_DPIX
      EndIf
    EndIf
    
    Prev_PB_Desktop_DPIX = 0
    Prev_PB_Desktop_DPIY = 0
    Prev_PB_Desktop_ResolutionX = 0
    Prev_PB_Desktop_ResolutionY = 0
    Prev_FontDPI = 0
    
  Else
    
    If dpi = 0
      dpi = GetSystemDPI()
    EndIf
    
    Prev_PB_Desktop_DPIX = PB_Desktop_DPIX
    Prev_PB_Desktop_DPIY = PB_Desktop_DPIY
    
    Prev_PB_Desktop_ResolutionX = PB_Desktop_ResolutionX
    Prev_PB_Desktop_ResolutionY = PB_Desktop_ResolutionY
    
    PB_Desktop_DPIX = dpi
    PB_Desktop_DPIY = dpi
    
    PB_Desktop_ResolutionX = dpi / 96
    PB_Desktop_ResolutionY = dpi / 96
    
    If *g_FontDPI
      Prev_FontDPI = *g_FontDPI\l
      *g_FontDPI\l = dpi
    EndIf
    
  EndIf
EndProcedure

Macro StopDPIAction()
  StartDPIAction(-1)
EndMacro




;- App startup.

LoadOSFunctions()

GetOSVersion(@g_OSVersion)

*g_FontDPI = GetFontDPIAddr()

; To receive the #WM_NCCREATE message, the callback must be set before any window is created.
If g_OSVersion\dwMajorVersion = 10 And (g_OSVersion\dwBuildNumber >= #WIN10_BUILD_1607 And g_OSVersion\dwBuildNumber < #WIN10_BUILD_1703)
  SetWindowCallback(@WinCallback_DPI())
EndIf


;- GUI

#wndMain  = 0

#wndMain_Font0 = 0

Procedure ResizeChildren(dpi.l)
  
  Static PrevFont
  Protected hFont
  Protected Font = LoadSystemDefaultFont(dpi)
  
  If Font = 0
    ProcedureReturn 0
  EndIf
  hFont = FontID(Font)
  
  
  StartDPIAction(dpi)
  
  ; If a font applied to a gadget is not the system default font, it must be loaded between the StartDPIAction and StopDPIAction blocks.
  ; And the font loaded here must not be applied to gadgets in other windows.
  LoadFont(#wndMain_Font0, "arial", 13, #PB_Font_Italic)
  
  ResizeGadget(0, 10, 10, 390, 220)
  
  SetGadgetAttribute(0, #PB_ScrollArea_InnerWidth, 575)
  SetGadgetAttribute(0, #PB_ScrollArea_InnerHeight, 555)
  
  ResizeGadget(1, 10, 10, 230, 40)
  ResizeGadget(2, 50, 50, 230, 30)
  ResizeGadget(3, 90, 90, 230, 30)
  ResizeGadget(4,130,130, 230, 20)
  
  SetGadgetFont(1, FontID(0))
  SetGadgetFont(2, hFont)
  SetGadgetFont(3, hFont)
  SetGadgetFont(4, hFont)
  
  StopDPIAction()
  
  If PrevFont And IsFont(PrevFont)
    FreeFont(PrevFont)
  EndIf
  
  PrevFont = Font
  
EndProcedure


Procedure WinCallback(hWnd, uMsg, wParam, lParam)
  
  If uMsg = #WM_EXITSIZEMOVE
    Debug "WM_EXITSIZEMOVE"
    
    StartDPIAction(GetDpiForWindow__(hWnd))
    Debug "Window W,H = " + WindowWidth(#wndMain) + " , " + WindowHeight(#wndMain)
    StopDPIAction()
  EndIf
  
  ; Windows 10 1703+
  If uMsg = #WM_GETDPISCALEDSIZE
    
    Debug "WM_GETDPISCALEDSIZE"
    
  EndIf
  
  ; Windows 8.1+
  If uMsg = #WM_DPICHANGED
    
    Debug "WM_DPICHANGED"
    
    Protected *rt.RECT
    
    *rt = lParam
    If *rt
      With *rt
        ; Change the window size.
        SetWindowPos_(hWnd, 0, \left, \top, \right - \left, \bottom - \top, #SWP_NOZORDER | #SWP_NOACTIVATE)
      EndWith
      
      ; Change the size of all child gadgets.
      ResizeChildren(wParam & $FFFF)
    EndIf
    
  EndIf 
  
  ProcedureReturn #PB_ProcessPureBasicEvents
EndProcedure

If OpenWindow(#wndMain, 0, 0, 405, 240, "ScrollAreaGadget", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  ScrollAreaGadget(0, 10, 10, 390,220, 575, 555, 30)
    ButtonGadget  (1, 10, 10, 230, 30,"Button 1")
    ButtonGadget  (2, 50, 50, 230, 30,"Button 2")
    ButtonGadget  (3, 90, 90, 230, 30,"Button 3")
    TextGadget    (4,130,130, 230, 20,"This is the content of a ScrollAreaGadget!",#PB_Text_Right)
  CloseGadgetList()
  
  ; Change the size of all child gadgets.
  ResizeChildren(GetDpiForWindow__(WindowID(#wndMain)))
  SetWindowCallback(@WinCallback(), #wndMain)
  
  Debug "Window W,H (init) = " + WindowWidth(#wndMain) + " , " + WindowHeight(#wndMain)
  
  
  Repeat
    Event = WaitWindowEvent()
  Until Event = #PB_Event_CloseWindow
EndIf