1. Vladimir K.
  2. PowerBuilder
  3. Friday, 1 November 2019 17:08 PM UTC

Hi all.

For a long time we been using environment object to check screen resolution.

But it does not provide any info on monitor scale factor ... 125%? 150% ... etc.

Has anyone already solve this or perhaps steer me in the right direction, perhaps win api's?

Been looking into GetSystemMetrics(), but it doesn't look promising.

Any help will be greatly appreciated.

Thank you.

Sincerely

Vlad

 

 

 

Michael Kramer Accepted Answer Pending Moderation
  1. Monday, 4 November 2019 21:13 PM UTC
  2. PowerBuilder
  3. # 1

Hi Miguel et al,

Sorry too much short hand in my code sample. Let me explain.

Background

  • FACT #1 :: I like C#/C++ enums they are just *SO* convenient to read when you need to understand code.
  • FACT #2 :: PowerScript compiler resolves all CONSTANT definitions at compile time.
    Therefore no need for runtime instantiation of any class containing just a list of constants.
  • FACT #3 :: Global functions are *SLOOOW* compared to instance level functions.
    See below for details.

Runtime - Slow Global Functions

  • Each global function is like a global class having no properties, no events, and only one function. Therefore =>
  • First time a global function is called:
    1. Its code is loaded into memory
    2. The global function object is "instantiated"
    3. Function executes
    4. IMMEDIATELY after code completes, global function is no longer referencen, so it is garbage collected and its memory released
  • Next time same global function is called:
    1. All same action as first time
    2. This repeat; each time you need the global function it is read from disk
    3. ALMOST every time: App won't reclaim memory until app has time for garbage collection which happens every half second. SO you MAY just get lucky that sometimes code is reused.
    4. But in general: Global function forces read from disk + instantiate at every single call.

Putting any function in a class loads that code when class instantiates. Function remains in memory until all instances are destroyed.

PFC uses utility NVOs encapsulated many, many functions to allow much faster runtime calls since code remains in memory.

Runtime - Enum NVOs

  • Every constant declaration is calculated at compile time => No runtime cost.
  • Every constant reference is resolved at compile time => No runtime cost.
  • You therefore never have to instantiate an object of a class to reference that class's instance constants!

My enum coding technique

  • I create one NVO class for each enum I want to use in my app.
    • Each such NVO has list of constants in its Instance Variables.
    • No other code.
    • AutoInstantiate = FALSE.
    • I my app uses 100 different C enums then I have 100 custom class NVOs Each having a constant for each value in the enum.
  • I name the class exactly the same as the enum to emulate. No prefix like "n_", "nvo_", or "n_pfc_". 
  • Here follows ll code I wrote for custom class = MONITOR_DPI_TYPE.
  • Notice only constants defined as instance variable, no methods at al nor variables
// Instance variables for Custom class NVO = MONITOR_DPI_TYPE
constant long EFFECTIVE_DPI = 0
constant long ANGULAR_DPI = 1
constant long RAW_DPI = 2
constant long DEFAULT = 3
  • Example of how to use =>
    • NOTICE: No instantiation required because all constants are resolved at compile time.
. . .
result = this._GetDpiForMonitor(..., MONITOR_DPI_TYPE.EFFECTIVE_DPI, ...)

Runtime - Calling Windows APIs

Calling a Windows API often requires extra plumbing.

  • You have to obtain handles
  • You need to pre-allocate string returned
  • You need to map between your PowerScript classes and the structs, enums, and classes used  Windows API
  • Most often you need to make several calls

So my implementation technique is:

  • Create a custom NVO class encapsulating a desired set of Windows API functions
  • Define all relevant API functions as PRIVATE LOCAL EXTERNAL functions
    • This ensures no other code anywhere in my app accidentally calls an API in a way that corrupts or crashes my app. Instead, my code has full control since all "dangerous" calls are PRIVATE.
  • Create PUBLIC instance function on this custom NVO for each set of functionality I need
    • Name this function wisely (EX: GetDpiScaleForWindow)
    • Let this function do all low level handling
    • Wrap subsets of functionality into separate functions when relevant (EX: GetMonitor(window))

Conclusion - Coding Style

  • I code with many, many NVOs.
  • Most of them very small, each having a single responsibility.
  • I limit access using PRIVATE and PROTECTED wherever appropriate to only expose safe APIs to remainder of my apps.
  • I use enums in large numbers => More NVOs!
  • WHen I code my NVOs I prefix by "this." a lot.
    • This ensures code remains functional even if it is incorporated in some other app accidentally having a global function or varible using same name.

 

This hopefully gives you some background. I'm sorry my shorthand for enums was too confusing. My mistake. I will extract this code into a separate .PBL and include that library in a tiny sample app to show how you may use my style of NVO design.

HTH /Michael 

Comment
  1. Michael Kramer
  2. Tuesday, 5 November 2019 08:51 AM UTC
Eureka! I suddenly realize what behavior change might have caused the better performance. Class nvo_mining calls gf_dig. So there is a dependency -- read: nvo_mining can't run without gf_dig. Hence, when loading nvo_mining the PB VM will most probably also load dependencies to ensure the code will actually run. Voilá! Global function loads and remains in memory until all calling classes are reclaimed. At least roughly.

Simple tuning:: In your app object -- create ue_preloader -- unmapped event -- call every global function in this event throwing in whatever local variables are needed from compiler's view point. This event never executes. However, just referencing the global functions may preload all of them into memory. Great!

Unfortunately this means you cannot ever bugfix any of them by adding a .PBD to front of library list using SetLibraryList - since all global functions are already in memory and remain their.

Performance tuning is complex! -- and I better get my facts straight before committing to this isn actual behavior. -- experiments; fact-finding; etc. before actually writing the articles.
  1. Helpful
  1. Miguel Leeuwe
  2. Tuesday, 5 November 2019 10:19 AM UTC
:) You lost me here, with "gf_dig" but anyway. Let's cut it off here, as I agree with you that this discussion should be elsewhere.

Thanks for all the info and doing a great job Michael !
  1. Helpful
  1. Miguel Leeuwe
  2. Tuesday, 5 November 2019 13:24 PM UTC
As a last remark: I tried with a real time application.

Very interesting to find that my findings of global functions only loading once are correct but ..... NOT for global functions used in datawindow attributes. In that last case they ARE reloaded many times when you come back in a window.

The solution has been what Michael already found out:

In my application manager object I have this IF:

IF 1 = 2 THEN

// all of my global function calls which I use.

END IF

That way, also for those used in datawindow expressions / attributes, are only loaded ONCE.

  1. Helpful
There are no comments made yet.
Vladimir K. Accepted Answer Pending Moderation
  1. Monday, 4 November 2019 20:07 PM UTC
  2. PowerBuilder
  3. # 2

Thank You Michael.

Thank You John.

Thank you Roland.

Guys, GREATLY appreciated your help.

Sincerely

Vladimir

 

 

 

 

 

 

Comment
There are no comments made yet.
Michael Kramer Accepted Answer Pending Moderation
  1. Monday, 4 November 2019 07:49 AM UTC
  2. PowerBuilder
  3. # 3

Hi Vlad,

Your PB app is a so called "DPI unaware" win32 app. Hence, it will receive DPI = 96 no matter what DPI function you call.

Function GetScaleFactorForMonitor returns an "old" scaling value: 100, 140, or 180 - differing from actual scaling. Following code calculates the scale setting correctly.

WARNING :: SetTheadDpiAwarenessContext requires Windows 10 (version 1607 or newer)!

// PRIVATE local external functions
function ulong	_GetDpiForMonitor (long hMonitor, long monitorDpiType, REF ulong dpiX, REF ulong dpiY) library "SHCore.dll" alias for "GetDpiForMonitor"
function long	_MonitorFromWindow (long hWindow, ulong dwFlags) library "user32.dll" alias for "MonitorFromWindow"
function long	_SetThreadDpiAwarenessContext (long dpiContext) library "user32.dll" alias for "SetThreadDpiAwarenessContext"
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
// ENUM NVOs
NVO = DEVICE_SCALE_FACTOR   : constant long INVALID = 0
NVO = DPI_AWARENESS_CONTEXT : constant long PER_MONITOR_AWARE = -3
NVO = HRESULT               : constant long S_OK = 0
NVO = MONITOR               : constant long DEFAULTTOPRIMARY = 1
NVO = MONITOR_DPI_TYPE      : constant long EFFECTIVE_DPI = 0
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
// NVO PUBLIC FUNCTION long GetMonitor (window aw_window)
return this._MonitorFromWindow(Handle(aw_window), MONITOR.DEFAULTTOPRIMARY)
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
// NVO PUBLIC FUNCTION long GetDpiScaleForWindow (window aw_window)
long hMonitor, result, scale, priorCtx
ulong dpiX, _

hMonitor = this.GetMonitor(aw_window)
priorCtx = this._SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE)
result = this._GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.EFFECTIVE_DPI, REF dpiX, REF _)
this._SetThreadDpiAwarenessContext(priorCtx)

if result = HRESULT.S_OK then
   scale = Round(((dpiX * 100) / 96), 0)
else
   scale = DEVICE_SCALE_FACTOR.INVALID
end if
return scale

Notes:

  • You will see scale = 100% for every call unless function changes DPI awareness to PER_MONITOR_AWARE.
  • You risk seeing app redraw incorrectly if you forget to restore DPI awareness to UNAWARE.
  • GetDpiForWindow returns 96 (= 100%) despite thread being DPI aware.
  • I create ENUM NVO for each code list in use. This enables code looking similar to C#/C++.

HTH /Michael

Comment
  1. Miguel Leeuwe
  2. Monday, 4 November 2019 11:39 AM UTC
Hi Michael, maybe a dumb question, but how do you "create ENUM NVO" in powerbuilder ?

Do you mean you create a nvo called DEVICE_SCALE_FACTOR?



NVO = DEVICE_SCALE_FACTOR : constant long INVALID = 0

Does not compile or anything from PB script.

Cheers
  1. Helpful
  1. John Fauss
  2. Monday, 4 November 2019 14:53 PM UTC
Nice work, Michael! Thank you!
  1. Helpful
  1. Vladimir K.
  2. Monday, 4 November 2019 20:25 PM UTC
Hi Michael.

First - Thank You.

Second - on Miguel question, i am with him, i also do not understand this scripting.

I am also not sure, why there is a need to create NVO's?

Why can we just declare API's in application object, create function f_check_scale() returns int (100, 125, .... etc) with the script inside to calls to those external functions and do the calc.

And then, we just call this f_check_scale() when we need it in pb script.



My apology if i am missing some major concept.



Thank you
  1. Helpful
There are no comments made yet.
Michael Kramer Accepted Answer Pending Moderation
  1. Monday, 4 November 2019 04:33 AM UTC
  2. PowerBuilder
  3. # 4

Hi Vlad,
I have sample app calling MonitorFromWindow; GetScaleFactorForMonitor (see code below).
PB app being win32 receives "old" scaling factor that differs from actual scaling factor.

Set Scale % 100 125 150 175 200 225 250
API returns 100 100 140 140 180 180 180

Due to blog post on High DPI I will dig further for potentially more precise scale info.

PRIVATE: // Local external functions
function long _GetScaleFactorForMonitor (long hMonitor, ref long device_scale_factor) library "Shcore.dll" alias for "GetScaleFactorForMonitor"
function long _MonitorFromWindow (long hWindow, ulong dwFlags) library "user32.dll" alias for "MonitorFromWindow"

// ENUM NVOs -- -- -- -- -- --
NVO = MONITOR             : constant long DEFAULTTOPRIMARY = 1
NVO = HRESULT             : constant long S_OK = 0
NVO = DEVICE_SCALE_FACTOR : constant long INVALID = 0

// NVO PUBLIC function long GetScaleFactorForWindow(window aw_window)
long default, hMonitor, hWindow, result, scale

hWindow = Handle(aw_window)
default = MONITOR.DEFAULTTOPRIMARY

hMonitor = this._MonitorFromWindow(hWindow, default)
result   = this._GetScaleFactorForMonitor(hMonitor, REF scale)

if result <> HRESULT.S_OK then
   scale = DEVICE_SCALE_FACTOR.INVALID
end if
return scale

/Michael

UPDATE 04-Nov: I now have solution that calculates scaling correctly. It requires temporarily turning on state = "DPI Aware" of UI thread to obtain DPI that differs from 96! And immediately restore state = "DPI Unaware" since the PB app is win32 app. I will describe in separate reply including updated source code.

 

Comment
  1. Roland Smith
  2. Monday, 4 November 2019 14:39 PM UTC
Having a API function based solution that works would be great. For my Bitmap capture example app I pull it from the registry. See the of_dpifactor function in n_bitmap.

http://www.topwizprogramming.com/freecode_bitmap.html
  1. Helpful
There are no comments made yet.
John Fauss Accepted Answer Pending Moderation
  1. Sunday, 3 November 2019 02:43 AM UTC
  2. PowerBuilder
  3. # 5

Well, I did some research last night and discovered that Microsoft has been making a series of changes to how Windows manages adjustments (both manual and automatic) to DPI scaling since Windows 8.1. Automatic adjustments in scaling occurs when tablet users dock/undock to/from single or multi-monitor workstations, for example. Higher resolution monitors and displays also have made an impact on this issue. In later versions, Windows no longer requires a Logoff/Logon to make a scaling adjustment, but instead handles such changes on the fly.

There are many new API functions to support this new functionality. Apps can now individually manage DPI scaling themselves with this functionality.

A consequence of these changes is that the traditional method of asking Windows for DPI scaling information ALWAYS returns a value of 96 dots/logical inch, regardless of the actual setting. As I understand it, this was done so as not to break the zillions of existing Windows applications. This explains why Vlad consistently gets the results he does. I used Windows 7 OS to create the code I offered, which works as intended in that and in earlier versions of Windows.

It is not immediately clear how to use the new API functions to obtain the "real" DPI scaling that is in effect for an application. As usual, Microsoft doesn't do a great job of providing sample code. To add to the complexity, the functionality and the corresponding API set has evolved over several Windows releases, so some API's cannot be used prior to Win8.1 and others prior to Win10 (1607) and there may be some critical releases in between. Interrogation code will have to first determine the OS version and build in order to know what API's are available. Roland's OSVersion free code example on the TopWizProgramming web site will help some. It's not rocket science, but this will take some experimentation to figure out. It looks like the new GetDpiForSystem, GetDpiForMonitor and GetDpiForWindow API's and possibly others will need to be used.

I will work on this as my spare and evening time permits, but cannot make any promises when I will have a solution code that works across all versions of the Windows OS. Others are welcome and encouraged to come up with a solution.

John

Comment
  1. Miguel Leeuwe
  2. Sunday, 3 November 2019 02:53 AM UTC
This is great John.

Yeap ... MS really is doing a very good job with it's DPI changes. It's been over a year that many applications which worked perfectly well, now all have the problem of blurry interfaces when the scaling is set to for example 125%. They were supposed to have solved it, but they haven't.

By the way, windows 8.1 was still working fine. The problems started in windows 10.
  1. Helpful
  1. Roland Smith
  2. Sunday, 3 November 2019 03:13 AM UTC
I ran into this for my Bitmap example. I ended up finding the dpi factor in the registry.

There is a function to make the app 'DPI Aware'.
  1. Helpful
There are no comments made yet.
Vladimir K. Accepted Answer Pending Moderation
  1. Friday, 1 November 2019 22:24 PM UTC
  2. PowerBuilder
  3. # 6

Still not working for me.

I am running pb2019 on latest win 10

display resolution 3840x2160
scale ( change the size of text, apps, and other items) = 250%

I attached screenshot.

Maybe because it is Friday eve and "ITS" trying to tell me it is time to shutdown and close the lid laughing

But thank you very match again and have a good weekend.

Sincerely

Vladimir

Attachments (1)
Comment
There are no comments made yet.
John Fauss Accepted Answer Pending Moderation
  1. Friday, 1 November 2019 21:53 PM UTC
  2. PowerBuilder
  3. # 7

Here's the PowerScript code copied directly from a test app I created:

Constant ULong LOGPIXELSX = 88, LOGPIXELSY=90
ULong lul_dpi_x, lul_dpi_y, lul_percent
Longptr llptr_hwnd, llptr_hdc

llptr_hwnd = GetDesktopWindow() // Get handle to desktop window
llptr_hdc  = GetDC(llptr_hwnd)  // Get handle to device context for desktop window.
lul_dpi_x  = GetDeviceCaps(llptr_hdc,LOGPIXELSX) // 96 is normal horz. dots/inch
lul_dpi_y  = GetDeviceCaps(llptr_hdc,LOGPIXELSY) // 96 is normal vert. dots/inch
lul_rc     = ReleaseDC(llptr_hwnd,llptr_hdc) // Release the device context.
lul_percent = (lul_dpi_x * 100) / 96
MessageBox('Logical Screen DPI Values','X = '+string(lul_dpi_x) + ', Y = '+string(lul_dpi_y)+'~r~n~r~n'+string(lul_percent)+'%')

and I've attached a screen shot of the results. Make sure the percent calculation matches what is shown above.

 

Attachments (1)
Comment
There are no comments made yet.
Vladimir K. Accepted Answer Pending Moderation
  1. Friday, 1 November 2019 21:29 PM UTC
  2. PowerBuilder
  3. # 8

Hi Roland.

It was nice to personally meet you at the conference.

Am i looking in the wrong place?

Attachments (1)
Comment
  1. Roland Smith
  2. Saturday, 2 November 2019 02:47 AM UTC
It is under the monitor key. Take a look at my Bitmap capture example in the of_dpifactor function.
  1. Helpful
There are no comments made yet.
Vladimir K. Accepted Answer Pending Moderation
  1. Friday, 1 November 2019 21:15 PM UTC
  2. PowerBuilder
  3. # 9

Hi John.

Thank you very match.

But for some reason regardless of changing scaling percent, ll_percent always return 100

Am i doing something wrong?

Is something wrong with llptr_hdc ?

Attachments (1)
Comment
There are no comments made yet.
John Fauss Accepted Answer Pending Moderation
  1. Friday, 1 November 2019 18:53 PM UTC
  2. PowerBuilder
  3. # 10

Hi, Vlad -

It's not in System Metrics. You'll need the following external function declarations:

FUNCTION Longptr GetDC ( Longptr hWnd ) LIBRARY "user32.dll"
FUNCTION Longptr GetDesktopWindow ( ) LIBRARY "user32.dll"
FUNCTION UnsignedLong GetDeviceCaps ( Longptr hDC, UnsignedLong nIndex ) LIBRARY "gdi32.dll"
FUNCTION Long ReleaseDC ( Longptr hWnd, Longptr hDC ) LIBRARY "user32.dll"

Then code to call them:

Constant ULong LOGPIXELSX = 88, LOGPIXELSY = 90
Long ll_rc, ll_percent

ULong lul_dpi_x, lul_dpi_y
Longptr llptr_hwnd, llptr_hdc

llptr_hwnd = GetDesktopWindow() // Get handle to desktop window
llptr_hdc  = GetDC(llptr_hwnd) / / Get handle to desktop's device context.

// 96 dots/inch is normal (100%), 120 is 125%, 144 is 150%.

lul_dpi_x  = GetDeviceCaps(llptr_hdc,LOGPIXELSX)
lul_dpi_y  = GetDeviceCaps(llptr_hdc,LOGPIXELSY)
ll_percent = (lul_dpi_x * 100) / 96

ll_rc = ReleaseDC(llptr_hwnd,llptr_hdc) // Release the device context.

Regards,
John

Comment
  1. Roland Smith
  2. Friday, 1 November 2019 20:09 PM UTC
In my bitmap capture example I was getting it from the registry under HKEY_CURRENT_USER\Control Panel\Desktop\PerMonitorSettings.

Using these functions is probably better and less error prone. I hope you don't mind if I incorporate your code, I'll put your name in the comments.
  1. Helpful
  1. John Fauss
  2. Friday, 1 November 2019 20:22 PM UTC
You're welcome to use it. Thanks for letting me know!
  1. Helpful
There are no comments made yet.
Michael Kramer Accepted Answer Pending Moderation
  1. Friday, 1 November 2019 18:36 PM UTC
  2. PowerBuilder
  3. # 11

Hi Vladimir, I haven't had the issue myself. - However, if I had to deal with scaling I would look for new win API exposing the current settings. Tjis could involve several new API calls.  I would define such external functions as private local externals on some utility NVO; then write a wrapper function acting as a facade.

HTH /Michael

Comment
  1. Vladimir K.
  2. Friday, 1 November 2019 21:23 PM UTC
  1. Helpful
  1. Michael Kramer
  2. Friday, 1 November 2019 22:42 PM UTC
Hi, this looks like the right functions, yes. A few days probably pass before I return with working sample - mostly offline visiting family after a week focused on Elevate. In the mean time it looks like John has a working solution for immediate fix.

Side note: These functions are excellent examples of where PowerScript "enum" NVOs come in handy for readable code.
  1. Helpful
There are no comments made yet.
  • Page :
  • 1


There are no replies made for this question yet.
However, you are not allowed to reply to this question.