DPI info

Started by MrBcx, December 23, 2019, 02:12:56 PM

Previous topic - Next topic

MrBcx

I didn't expect to spend any time today thinking about DPI but sometimes these things just happen. 
It started when my attention was diverted to this page:

https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows

I (probably incorrectly) concluded that any DPI scaling that I may find myself using in the future would likely benefit most from what Microsoft terms Per-Monitor-Version-2 (PMV2). 

I put together the following routine that is based on information in the MS article.  It's worth noting that because the Windows 10 API function GetDpiForWindow() is the secret sauce in this routine, the only way to get at that function is to call it dynamically through User32.dll since Pelles, Mingw, and presumably every other compiler except Microsoft's do not have the latest API headers and libraries needed.  I had nearly forgotten how useful Dynacall is.

Anyway, the SUB below could be used during the creation of any window (form, button, listbox, etc) and also by monitoring the WM_DPICHANGED message [WM_DPICHANGED = 0x02E0] for when a user changes DPI settings while your app is running.


' The other Windows 10 DPI functions mentioned in the article could be similarly invoked, if you need them:
'  GetSystemMetricsForDpi       (lib "user32.dll", ...
'  AdjustWindowRectExForDpi   (lib "user32.dll", ...
'  SystemParametersInfoForDpi (lib "user32.dll", ...


DIM a as HWND

a = GetConsoleWindow()

PRINT GetDpiForWindow (lib "user32.dll", a)   ' returns 96 on my PC


'********************************************************************
' This SUB could be useful for scaling all user created windows at
' startup and while running
'********************************************************************


SUB DPI_Correction_PWV2 _
(hWnd AS HWND,     _      ' HWND to modify for DPI scale
  OrigX AS INTEGER, _      ' 96 Based DPI x ordinate of window ctrl
  OrigY AS INTEGER, _      ' 96 Based DPI y ordinate of window ctrl
  OrigW AS INTEGER, _      ' 96 Based DPI width of window ctrl
  OrigH AS INTEGER  )      ' 96 Based DPI height of window ctrl
'********************************************************************
  LOCAL iDpi            AS INTEGER
  LOCAL dpiScaledX      AS INTEGER
  LOCAL dpiScaledY      AS INTEGER
  LOCAL dpiScaledWidth  AS INTEGER
  LOCAL dpiScaledHeight AS INTEGER
'********************************************************************
  iDpi = GetDpiForWindow (lib "user32.dll", hWnd)
'********************************************************************
  dpiScaledX      = MulDiv(OrigX, iDpi, 96)
  dpiScaledY      = MulDiv(OrigY, iDpi, 96)
  dpiScaledWidth  = MulDiv(OrigW, iDpi, 96)
  dpiScaledHeight = MulDiv(OrigH, iDpi, 96)
'********************************************************************
  SetWindowPos _
  (hWnd, hWnd, dpiScaledX, dpiScaledY, dpiScaledWidth, dpiScaledHeight, _
  SWP_NOZORDER | SWP_NOACTIVATE)
END SUB



Jeff

Good stuff.  DPI scaling is very...interesting.  Thank you for getting this topic started. 

Would you mind trying to compile your code for 64-bit with Pelles?  I'm getting a warning and an error, but may just have something wrong with my setup.  32-bit works fine.

MrBcx

Jeff -- It appears the inline assembly part of our dynacall implementation (currently) only works for 32-bit.

If I get some time, I'll see what changes are needed to make it 64bit capable.

Also, it's not just Pelles ... Mingw won't compile it for 64-bit either and likely the others.

MrBcx

Here is a method for dynamically loading GetDpiForWindow that doesn't use Dynacall.

This method compiles and runs correctly using Mingw and Pelles when compiled as 32-bit and 64-bit.

If compiling for 32-bit use this:
   hUser32 = LOADLIBRARY("c:\windows\syswow64\user32.dll") 'For 32-bit

If compiling for 64-bit use this:
  hUser32 = LOADLIBRARY("c:\windows\system32\user32.dll")  'For 64-bit

That's right ... Microsoft has a 32-bit and a 64-bit version of User32.dll and both are named User32.dll




$HEADER
typedef int(__stdcall *GETDPI)(HWND);
$HEADER



DIM a AS HWND

a = GetConsoleWindow()

PRINT GetDpiForWindow_Dynamic(a)

PAUSE


FUNCTION GetDpiForWindow_Dynamic (hwnd AS HWND)
  DIM hUser32 AS HMODULE
  DIM funky AS GETDPI

   hUser32 = LOADLIBRARY("c:\windows\syswow64\user32.dll") ' [b]For 32-bit[/b]
'  hUser32 = LOADLIBRARY("c:\windows\system32\user32.dll") ' [b]For 64-bit[/b]

  IF NOT hUser32 THEN
    PRINT  "Could not load user32.dll"
    END
  END IF

  funky = GetProcAddress(hUser32, "GetDpiForWindow")

  IF NOT funky THEN
    PRINT  "Could not locate the function: GetDpiForWindow()"
    END
  END IF
  FUNCTION = funky(hwnd)
END FUNCTION


Robert

VERY useful!

Thank you very much MrBCX.

May we all have a Merry Christmas and Happy New Year.

MrBcx

Placing the typedef inside the function that uses it appeals to me on several points.
One point being that others like it inside the function too ... "It's good. It's legal and localized."
https://stackoverflow.com/questions/10103453/is-typedef-inside-of-a-function-body-a-bad-programming-practice

This could serve as a useful model for other API candidates that don't exist in current C headers/libraries.



FUNCTION GetDpiForWindow_Dynamic (hwnd AS HWND)
'**********************************************
! typedef int(__stdcall *GETDPI)(HWND);
'**********************************************
  DIM hUser32 AS HMODULE
  DIM funky AS GETDPI

  hUser32 = LOADLIBRARY("c:\windows\syswow64\user32.dll") ' For 32-bit
' hUser32 = LOADLIBRARY("c:\windows\system32\user32.dll") ' For 64-bit

  IF NOT hUser32 THEN
    PRINT  "Could not load user32.dll"
    FUNCTION = 0
  END IF

  funky = GetProcAddress(hUser32, "GetDpiForWindow")

  IF NOT funky THEN
    PRINT  "Could not locate the function: GetDpiForWindow()"
    FUNCTION = 0
  END IF
  FUNCTION = funky(hwnd)
END FUNCTION



Jeff

Even better!  Thanks again for this useful snippet.

I might simply be getting lucky, but this is working fine for me on 32 and 64-bit:
hUser32 = LOADLIBRARY("user32.dll")

Is this a bad idea?


I was messing with DPI stuff a couple of weeks ago myself, and I applied the approach I used for that Win10 detection function to GetDpiForWindow.
FUNCTION GetDpiForWindow_Jeff(hWnd AS HWND)
  DIM iReturn
  !NTSTATUS (WINAPI *GetDpiForWindow)(HWND);
  !*(FARPROC*)&GetDpiForWindow = GetProcAddress(GetModuleHandle("user32"), "GetDpiForWindow");
  IF GetDpiForWindow THEN
    iReturn = GetDpiForWindow(hWnd)
  ELSE
    PRINT  "GetDpiForWindow function not found"
    iReturn = 0
  END IF
  FUNCTION = iReturn
END FUNCTION


I like parts of each approach, and they both seem to work fine.  So I'm just wondering if one method is safer/more stable than the other?


MrBcx

Quote from: Jeff on December 25, 2019, 11:34:27 AM

I might simply be getting lucky, but this is working fine for me on 32 and 64-bit:
hUser32 = LOADLIBRARY("user32.dll")

Is this a bad idea?

I like parts of each approach, and they both seem to work fine.  So I'm just wondering if one method is safer/more stable than the other?

Not for me to say ... either approach may fail in the future, as long as MS has a role in this.



MrBcx

One new function and one updated function.

I can't say how useful these are but they may find a home in your DPI toolbox.




PRINT GetDpiForWindow_Dyn (GetConsoleWindow())   ' returns 96 on my PC

PRINT SetProcessDPIAware_Dyn()                   ' returns 1  on my PC

PAUSE



FUNCTION GetDpiForWindow_Dyn (hwnd AS HWND)
'***************************************************
' Returns the current DPI for the Window parameter
' For example, this function returns 96 DPI on my
' Windows 10 screen (1920x1080) at default DPI
'***************************************************
! typedef int(__stdcall *GETDPI)(HWND);
'***************************************************
  DIM hUser32 AS HMODULE
  DIM funky AS GETDPI
  LOCAL RetCode

  hUser32 = LOADLIBRARY(SYSDIR$ + "\user32.dll")

  IF NOT hUser32 THEN
    PRINT  "Could not load user32.dll"
    FUNCTION = 0
  END IF

  funky = GetProcAddress(hUser32, "GetDpiForWindow")

  IF NOT funky THEN
    PRINT  "Could not locate the function: GetDpiForWindow()"
    FreeLibrary(hUser32)
    FUNCTION = 0
  END IF

  RetCode = funky(hwnd)
  FreeLibrary(hUser32)
  FUNCTION = RetCode
END FUNCTION




FUNCTION SetProcessDPIAware_Dyn () AS BOOLEAN
'***************************************************
' SetProcessDPIAware
' Returns TRUE on success, FALSE on failure
'***************************************************
  LOCAL hUser32 AS HMODULE
  LOCAL funky   AS FARPROC
  LOCAL RetCode

  hUser32 = LOADLIBRARY(SYSDIR$ + "\user32.dll")

  IF NOT hUser32 THEN
    PRINT  "Could not load user32.dll"
    FUNCTION = 0
  END IF

  funky = GetProcAddress(hUser32, "SetProcessDPIAware")

  IF NOT funky THEN
    PRINT  "Could not locate function: SetProcessDPIAware"
    FreeLibrary(hUser32)
    FUNCTION = 0
  END IF

  RetCode = funky()
  FreeLibrary(hUser32)
  FUNCTION = RetCode
END FUNCTION


Ian Casey (RIP)

Hi Kevin,
Thanks for those functions.  I don't have a way to test other than 96dpi at present.

I was wondering if one could set the dpi in each control using MoveAnchorControl?
I use MoveAnchor in most of my programs and already have most controls there for sizing/moving.
Anyway it's just a thought, and maybe something to look at when I get some time.

Ian

MrBcx

Quote from: iancasey on December 28, 2019, 08:43:54 PM
Hi Kevin,
Thanks for those functions.  I don't have a way to test other than 96dpi at present.

I was wondering if one could set the dpi in each control using MoveAnchorControl?
I use MoveAnchor in most of my programs and already have most controls there for sizing/moving.
Anyway it's just a thought, and maybe something to look at when I get some time.

Ian


Hi Ian,

There is much to absorb on the subject of Windows and DPI and I've barely scratched the surface but I think the answer to your question is, "no"

The SetProcessDPIAware function was first available in MS Vista and I believe what I read was that one must call that function only ONE TIME before ANY hwnd is created in your program.  There is a newer version for Windows 10 (...PWV2) but I haven't found a way to call it yet with the parameters it needs.

In my opinion, MS is making every Windows programmer on the planet jump through hoops trying to accomplish something that the operating system should be properly handling on its own ... namely, scaling.

Jeff

Quote from: MrBcx on December 29, 2019, 07:26:14 AM
The SetProcessDPIAware function was first available in MS Vista and I believe what I read was that one must call that function only ONE TIME before ANY hwnd is created in your program.  There is a newer version for Windows 10 (...PWV2) but I haven't found a way to call it yet with the parameters it needs.

In my opinion, MS is making every Windows programmer on the planet jump through hoops trying to accomplish something that the operating system should be properly handling on its own ... namely, scaling.

This is overkill for what you're looking for, but...here's everything I've put together so far for DPI stuff in Win10 (plus a few other things).  I need to take a break from it, but hopefully this will help the effort.  Sorry about the lack of comments in the code, but just ask if anything isn't clear.

I may edit this post with more info if I have time, but here's the quick info:
main code is in dpi.bas.  (dpi32.bas and dpi64.bas are there just to make compiling quicker)

When you click the button, a file will be created containing interesting bits of info about dpi awareness, scales, etc.  If you have multiple monitors, and especially if they're at different resolutions and/or scales, you'll better appreciate the info it collects.  If you launch with one of the bat files, it will attempt to change the awareness when you click the button.  Just run it a few different ways and it should start to make sense.  What I've found is if the process is unaware at launch, I can change the awareness once without issue.  But if it's already set via manifest/resource, it won't work.  (The included dpi64-pmv2.exe is just a copy of dpi64.exe that will use the included manifest...PMV2).  Short version, setting awareness from inside the program appears to be a one time shot.  And, at least in this simple example, it doesn't seem to matter if the hwnd exists yet or not. 

Notable functions include:
GetDpiForWindow_Dynamic
SetProcessDpiAwarenessContext_Dynamic
SetThreadDpiAwarenessContext_Dynamic
GetWindowDpiAwarenessContext_Dynamic
GetThreadDpiAwarenessContext_Dynamic
AreDpiAwarenessContextsEqual_Dynamic

Have fun.  :)

(attachment: dpi stuff.zip)


MrBcx

Lots of useful info Jeff ...

And thanks especially for these definitions ... I was having a tough time tracking these down:



#define _DPI_AWARENESS_CONTEXTS_

DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);

typedef enum DPI_AWARENESS {
    DPI_AWARENESS_INVALID           = -1,
    DPI_AWARENESS_UNAWARE           = 0,
    DPI_AWARENESS_SYSTEM_AWARE      = 1,
    DPI_AWARENESS_PER_MONITOR_AWARE = 2
} DPI_AWARENESS;


Jeff

Quote from: MrBcx on December 25, 2019, 12:10:13 PM
Quote from: Jeff on December 25, 2019, 11:34:27 AM

I might simply be getting lucky, but this is working fine for me on 32 and 64-bit:
hUser32 = LOADLIBRARY("user32.dll")

Is this a bad idea?

I like parts of each approach, and they both seem to work fine.  So I'm just wondering if one method is safer/more stable than the other?

Not for me to say ... either approach may fail in the future, as long as MS has a role in this.


I wanted to jump in real quick to say that I switched to your approach tonight.  I had absolutely no problem with either approach on Win10, but my approach ended up failing during testing on Win8.1.  So typedef, LoadLibrary, GetProcAddress, FreeLibrary is the way to go.  I should be ready to upload a simple yet complete fully DPI aware example before the weekend is over.  Stay tuned everyone.

Jeff

MrBcx

#14
Thanks for the update Jeff ... I recently set my scaling to 125% ( easier on the eyes ) and not as whacked out as the 150% that Microsoft recommended.  One of the first things I noticed is that my screenshotgrabber is in need of some DPI love when scaling is enabled, so that will be one of my next experiments once your toolkit is released.

Changing subjects, I'm getting really close to wrapping up 7.3.9.  I feel like I finally came up with a fix that addresses the extra "return DefWindowProc" statements and the incorrect overwriting of user-specified callback returns that Mike Henning pointed out.  That was way more bother than I ever imagined it would be.

I have all the CON_Demos, Gui_Demos, and DLL_Demos updated and compiling cleanly with Pelles C which is my personal favorite C environment to accompany BCX. 

Next up is to pare down and update the \bin\ folder from the BCX_install to include only 32 bit tools and help which will address some of Roland's issues.  I'm not going to make an installer -- everything will be individually zipped.