Email sent.
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 ) 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
Phew, that’s a fair bit for me to take in. I’ll see how I go. Thank you.