Get Monitor Scale Factor

Email sent. :slight_smile:

Hi David,

I am currently looking for a migration path for a DPI unaware app. After quite a bit of investigation (and stumbling across your timely post here :slight_smile:) I found that the following is the best approach for apps that are not DPI aware.

Note that telling Windows that your app is DPI aware by using the manifest file in your XE app could have other side effects. Either way you should test your app thoroughly on a system with at least two monitors that have different scaling factors, starting and dragging your app windows on different monitors and checking appearance, and particularly any code that uses screen/monitor position and size, window position and size, cursor position, etc.

From your post and posts in other forums it seems that GetScaleFactorForMonitor can be unreliable, so I decided to use GetDpiForMonitor after temporarily making the app DPI aware at runtime. You can then divide by 96 to calculate the scaling factor.

I am using Delphi 10.1 in this app so I can just call TMonitor.PixelsPerInch. It calls GetDpiForMonitor on Windows 8.1+ with dpiType = 0 (effective DPI), and falls back to GetDeviceCaps(GetDC(0), LOGPIXELSY) on older systems (returns DPI of the primary/main monitor). In XE you may need to call GetDpiForMonitor directly. It is implemented in Shcore.dll.

How to change DPI awareness at runtime:

uses
  Winapi.Windows;

type
  DPI_AWARENESS_CONTEXT = type THandle;

const
  DPI_AWARENESS_CONTEXT_UNAWARE              = DPI_AWARENESS_CONTEXT(-1);
  DPI_AWARENESS_CONTEXT_SYSTEM_AWARE         = DPI_AWARENESS_CONTEXT(-2);
  DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE    = DPI_AWARENESS_CONTEXT(-3);
  DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = DPI_AWARENESS_CONTEXT(-4);
  DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED    = DPI_AWARENESS_CONTEXT(-5);

function SetThreadDpiAwarenessContext(const DPIContext: DPI_AWARENESS_CONTEXT): DPI_AWARENESS_CONTEXT; stdcall;
function SetThreadDpiAwarenessContext; external User32 name 'SetThreadDpiAwarenessContext' delayed;

var
  origDpiAwarenessContext: DPI_AWARENESS_CONTEXT;
begin
  origDpiAwarenessContext := SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

  // Use Form.Monitor.PixelsPerInch or Screen.Monitors[i].PixelsPerInch etc
  // or call GetDpiForMonitor() directly

  SetThreadDpiAwarenessContext(origDpiAwarenessContext);
end;

(Edit: For other readers I should add that the function and values for DPI_AWARENESS_CONTEXT were added to Winapi.Windows in a later version of Delphi, at least they are present in Delphi 12.2, so the declarations above may not be required in the version of Delphi that you are using).

Note that while DPI awareness is temporarily enabled TMonitor.Width and TMonitor.Height (or GetMonitorInfo()) return scaled values. e.g. 1920x1080@150% monitor will return 2880x1620. If you need the absolute/raw pixel size of the monitor then use Monitor.Width/Height before or after temporarily enabling DPI awareness.

This function can also be used to temporarily disable DPI awareness in a DPI aware app, which can be useful when transitioning a previosuly DPI unaware app. e.g. In later versions of Delphi set the project options to DPI awareness = Per Monitor V2, or in earlier versions of Delphi it might work to call SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) before any form is displayed, otherwise use a custom manifest file - use ResourceHacker to check if Delphi is already adding one within your app which may be the case if you enable runtime themes in the project settings.

Then for any forms that you haven’t yet reviewed and updated to be DPI aware: a) call the function to set to unaware, b) create the form, c) set back to orig awareness (per monitor v2). That, supposedly, makes that particular form behave like before (unaware, at least in terms of automatic scaling/font substitution/common controls) while other updated forms in your app can remain fully DPI aware. That info is based on this article:

Cheers,
Jarrod

1 Like

Phew, that’s a fair bit for me to take in. I’ll see how I go. Thank you.