Scaling UI with DPI change of non MFC application - winapi

My application is a plug-in developed in VC++ (win32). The solutions I could find didn't work for me.
I have two options :
To disable DPI changes for my DLL plugin. That means my plug-in will not be affected with DPI changes.
To scale all the controls according to the DPI change.
Please help.
Thanks.

1. Disable DPI affect on my application UI when some one re-configures the DPI. Help required : If possible, how to do that pro-grammatically.
This is not possible. Adrian already told you that. DPI is a user setting, not an application setting. If the user wants to change their DPI, they can do so. They can also apply backwards-compatibility hacks that turn off high DPI for a particular application, but that functionality is not available to applications. As the developer, you're expected to make sure that you support high DPI environments, not disable it.
The only thing you can do as an app developer is to fail to indicate that your application is DPI-aware. This is roughly equivalent to walking around with an "IDIOT" sign taped to your forehead. You might find that people are slightly more accommodating of your shortcomings, but they won't have much respect for you and probably won't care to hang out with you very often. Windows will do much the same thing: it will hide the truth from you about the user's actual DPI settings (you can't handle the truth) and instead it will scale up your interface automatically to match. The effect is an ugly one.
Of course, you also say that you're creating a plug-in DLL, which changes things. DLLs can't alter the DPI-awareness of the entire process because that is determined by the host application. If the host application indicates that it is high DPI aware, then your DLL must also be high DPI aware. This is an all or nothing setting, just like the "IDIOT" sign.
2. Scale the UI of my application according to the DPI change. Help required : How to determine the current DPI and how to scale the UI accordingly? Is it necessary to scale every component or any other way to scale them automatically.
In order to correctly determine the current DPI settings, you need to indicate to Windows that you're not an idiot your application is high DPI aware. As discussed above, if you do not do this, Windows will assume that you cannot handle the truth and lie to you. But as we also established above, this probably does not apply to you because you're creating a DLL that runs in the context of another process that is already establishing itself as high DPI aware.
So, what you need to do is determine the DPI scale factor. You need that to scale your user interface elements accordingly when running in high DPI environments. The baseline DPI setting is taken to be 96. The following code scales a SIZE structure (that defines a width and height) relative to that baseline:
void ScaleDpi(SIZE& size)
{
// Determine the current screen DPI.
const HDC hDC = GetDC(NULL);
const int dpiX = GetDeviceCaps(hDC, LOGPIXELSX);
const int dpiY = GetDeviceCaps(hDC, LOGPIXELSY);
ReleaseDC(NULL, hDC);
// Perform the scaling.
size.cx = MulDiv(size.cx, dpiX, 96);
size.cy = MulDiv(size.cy, dpiY, 96);
}
However, if you follow good design practices, you may not need to scale every control manually. When defining windows and dialogs in a resource file (e.g., using the Visual Studio Dialog Editor), you specify the layout in logical units (known as DLUs, or dialog units). These units are independent of any particular DPI setting and are therefore preferable to using something like pixels. A simple dialog with a few controls therefore requires no special scaling effort. You'll only need to perform manual scaling if you're creating and/or laying out controls at run-time.
You'll find more tips and techniques on how to write high DPI aware applications on MSDN, in the Writing High-DPI Win32 Applications article. And I don't link this just to say RTFM—it's actually extremely helpful, and I highly recommend reading it all.
The folks who make the RealWorld Icon and Cursor editors also maintain a helpful article on their site: DPI-aware applications in Windows Vista and Windows 7.

Related

How to set DPI scale to less than 100% on Windows 10 - With multiple displays

So I have a big 32 inch display with a resolution of 1440p, and I want to set the DPI scaling to 75% instead of 100%. But I can't find any way to do so on multiple monitors.
I currently have:
Display 1 [2560 x 1440] (Main display I want to change)
Display 2 [2560 x 1440] (This one is 27 inches so it's fine as is)
Display 3 [3840 x 2160] (Set to 100%, fine as it is)
This trick (click me) changes DPI scaling via some registry keys (LogPixels & Win8DpiScaling), but when I use that trick it downscales display 3 instead of display 1.
Is there a way to get this to work? I see no reason for Microsoft to limit the scaling in displays.
Note: I have a 2070 super, all the displays are plugged into the GPU via displayport directly, with the latest avalible firmware at the time of writing (september 2021)
The tl;dr:
Technical limitations aside, there are very solid user experience reasons why this probably isn't allowed.
No, Windows will not let you set UI scaling below 100%.
(even if a stable workaround were to be discovered, most users would probably be quite unhappy with the results)
While I would love¹ to be proven incorrect, the implications of scaling at less than 100% are so fraught that this limitation is unlikely to change in the near future.
Background:
This has been the case for ages, likely since Windows first introduced the feature.
Compatibility with current software
The only ~purely technical~ reason I can think of:
The 100% scaling size likely uses the smallest base image (e.g. Explorer and Taskbar icons, mouse and text cursors) resources included in various existing Microsoft and 3rd-party applications.
User experience
Going below the 100% point may cause small UI text and icons, especially in application toolbars and the Taskbar to be blurred to the point of ambiguity.
Those fine lines in the taskbar 'Windows' menu icon? Blurred or gone.
Taken to the extreme, the UI ~might~ become so unreadable that the user is effectively prevented from being able to read the text even in the 'Settings' window and therefore is 'stuck': i.e. not able to navigate through 'Settings' to restore the original '100%' scaling mode.
(Luckily, Windows is never used to run any SCADA software where confusing two icons could theoretically cost money or lives.)
Performance:
Since those carefully-designed graphic assets don't exist, if sub-100% scaling were allowed, it would also likely cause extra CPU/GPU workload - that is why only certain fixed sizes of up-sampling are shown on the normal Display settings screen and why the Advanced scaling settings screen warns that custom scaling between 100-500% is "not recommended".
That might also apply to any fixed scaling option offered below 100%, and absolutely would for custom scaling sizes.
Some people enjoy reading:
Vector-based TrueType/OpenType fonts usually contain a ~lot~ of manual tweaking / hints to enable readable display of very small point sizes.
The marketing department & friends of the C-suite
Could they implement this at a limited range of options? 90%? 75%?
Perhaps - but it's extra testing for a horrible-looking edge case.
The existence of the option, even if only available as a registry hack, might cause some people to actually use it in kiosks and other public-facing displays; this risks the same sort of bad PR as when a BSOD is seen on the 'arrivals' screen at a train station or airport monitor.
Combined with the first example below, even a 90% option could cause trouble in some environments.
Example and tutorial:
Imagine how Windows might look displayed on one of those cheapo '1080p-supported' projectors that actually only contains an imager with a native pixel resolution of, say, 1024x576 (or even 480x234).
Windows thinks it can send 1080p, since that what the HDMI connection advertises, so it does: any text / vector content looks atrocious.
(At least in this case the user could normally² unplug the projector and reconnect to a normal monitor to restore functionality.)
See for yourself... while connected to any monitor (at that monitor's native resolution), with Windows set to 100% scaling:
Open Windows Notepad
Type or paste in any block of text
Now, use the Zoom Out command from the View menu³ five or more times in a row
While not an exact analogue, you may still see how hard it could be to read down-sampled text, even when very high-contrast (the best-case scenario).
   ¹: As someone currently typing this very answer on a 1080p connection to a 55" 4K television as a second monitor, I came across the question very much hoping this was possible. Sadly, logic intervened and killed my potential joy.
   ²: Unless the computer is actually stored somewhere locked or inaccessible, such as a NUC-style PC hidden above the false ceiling in a conference room.
   ³: Alternatively, press <CTRL>-<Minus> five or more times.

Can DPI scaling be enabled/disabled programmatically on a per-session basis?

My application happens to be written in Python using pygame, which wraps SDL, but I'm imagining that this is probably a more-general question to do with the Windows API.
In some of my Python applications, I want pixel-for-pixel control under Windows 10 even at high resolutions. I want to be able to ensure, for example, that if my Surface Pro 3 has a native resolution of 2160x1440, then I can enter full-screen mode with those dimensions and present a full-screen image of exactly those dimensions.
The barrier to this is "DPI scaling". By default, under Windows' Settings -> Display, the value of "Change the size of text, apps, and other items" is "150% (Recommended)" and the result is that I only see 2/3 of my image. I have discovered how to fix this behaviour...
systemwide, by moving that slider down to 100% (but that's undesirable for most other applications)
just for python.exe and pythonw.exe, by going to those executables' "Properties" dialogs, Compatibility tab, and clicking "Disable display scaling on high DPI settings". I can do this for me alone, or for all users. I can also automate this process by setting the appropriate keys in the registry programmatically. Or via .exe.manifest files (which also seems to require a global setting change, to prefer external manifests, with possible side-effects on other applications).
My question is: can I do this from inside my program on a per-launch basis, before I open my graphics window? I, or anyone using my software, won't necessarily want this setting enabled for all Python applications ever—we might want it just when running particular Python programs. I'm imagining there might be a winapi call (or failing that something inside SDL, wrapped by pygame) that could achieve this, but so far my research is drawing a blank.
Here's the answer I was looking for, based on comments by IInspectable and andlabs (many thanks):
import ctypes
# Query DPI Awareness (Windows 10 and 8)
awareness = ctypes.c_int()
errorCode = ctypes.windll.shcore.GetProcessDpiAwareness(0, ctypes.byref(awareness))
print(awareness.value)
# Set DPI Awareness (Windows 10 and 8)
errorCode = ctypes.windll.shcore.SetProcessDpiAwareness(2)
# the argument is the awareness level, which can be 0, 1 or 2:
# for 1-to-1 pixel control I seem to need it to be non-zero (I'm using level 2)
# Set DPI Awareness (Windows 7 and Vista)
success = ctypes.windll.user32.SetProcessDPIAware()
# behaviour on later OSes is undefined, although when I run it on my Windows 10 machine, it seems to work with effects identical to SetProcessDpiAwareness(1)
The awareness levels are defined as follows:
typedef enum _PROCESS_DPI_AWARENESS {
PROCESS_DPI_UNAWARE = 0,
/* DPI unaware. This app does not scale for DPI changes and is
always assumed to have a scale factor of 100% (96 DPI). It
will be automatically scaled by the system on any other DPI
setting. */
PROCESS_SYSTEM_DPI_AWARE = 1,
/* System DPI aware. This app does not scale for DPI changes.
It will query for the DPI once and use that value for the
lifetime of the app. If the DPI changes, the app will not
adjust to the new DPI value. It will be automatically scaled
up or down by the system when the DPI changes from the system
value. */
PROCESS_PER_MONITOR_DPI_AWARE = 2
/* Per monitor DPI aware. This app checks for the DPI when it is
created and adjusts the scale factor whenever the DPI changes.
These applications are not automatically scaled by the system. */
} PROCESS_DPI_AWARENESS;
Level 2 sounds most appropriate for my goal although 1 will also work provided there's no change in system resolution / DPI scaling.
SetProcessDpiAwareness will fail with errorCode = -2147024891 = 0x80070005 = E_ACCESSDENIED if it has previously been called for the current process (and that includes being called by the system when the process is launched, due to a registry key or .manifest file)

AccessibleObjectFromPoint and per-monitor DPI

I'm using accessibility with the AccessibleObjectFromPoint function, and I'd like it to work correctly on a per-monitor DPI environment. Unfortunately, I can't get it to work. I tried many things, and the situation for now is:
My app is marked as per-monitor-DPI-aware in the manifest. (True/PM)
I use GetCursorPos and then AccessibleObjectFromPoint.
How can the problem be reproduced:
Have two monitors, one with 100% DPI, the other with 125%.
Run Chrome on the 125% monitor.
Use AccessibleObjectFromPoint on one of the tab names, it won't work.
It works with some apps (DPI-aware, it seems, like explorer), but doesn't work with others. I tried several relevant functions, such as GetPhysicalCursorPos and PhysicalToLogicalPointForPerMonitorDPI, but nothing works.
It's worth noting that Microsoft's inspect.exe works as expected.
I’ve been struggling with this exact same problem for several weeks and can now tell you my findings. Unfortunately I can’t give you more than a hint of code, because the project I am working on, is proprietary.
The issue started at Windows 8.1. The problem did not exist on Windows 7 or Vista, because AccessibleObjectFromPoint always used raw physical coordinates, as documented here: https://msdn.microsoft.com/en-us/library/windows/desktop/dd317984(v=vs.85).aspx .
“Microsoft Active Accessibility does not use logical coordinates. The following methods and functions either return physical coordinates or take them as parameters.” This has not been true since Windows 8.1.
AccessibleObjectFromPoint now uses a flawed calculation that cannot always find the correct window for reasons similar to my question here: High DPI scaling, mouse hooks and WindowFromPoint .
My findings lead me to one conclusion: The API is broken. This does not mean it is not possible though.
Possible solutions I have partially tested that seem to work follow.
Prerequisites are that you
1/. Make your process per monitor DPI aware, NOT USING THE MANIFEST (more on that later).
2/. Determine the hWnd of the window you want to query (WindowFromPoint() variants)
3/. Determine the monitor DPI of the queried hWnd
4/. Determine the DPI of your process
5/. Determine the DPI of the queried hWnd
6/. Determine the monitor origin and offset for the queried hWnd (MonitorFromWindow() and GetMonitorInfo() )
Next, depends on your platform
Windows 10.0.14393+
Write a function that finds the IAccessible (AccessibleObjectFromWindow() ) from the top level window, and then recursively call IAccessible::accHitTest until you reach the bottom-most IAccessible and perhaps ChildID data. Return that as if you would call AccessibleObjectFromPoint.
To call it successfully, you will need to scale the (x,y) co-ordinates into the scale system of the queried hWnd, using the DPIs and co-ordinates fetched in the list above. Watch out for systems where monitors are not the same size or if monitors are partially offset, or above and below.
And now for the important part for 10.0.14393 – Set your thread to the same DPI_AWARENESS_CONTEXT of the hWnd you are querying. Now call your new function. Now revert your thread to monitor DPI aware, and voila, it works, even if the window is not maximised. This is why you must not use the manifest.
If you are on Windows 8.1 to 10.0.10586 you have a tougher task.
Instead of calling accHitTest, as above, you have to recursively call AccessibleChildren and iterate the call IAccessible::accLocation to determine if your test point is within each child. This is tricky and starts to get really messy when you get to e.g. combo boxes in products like Office, which is only system DPI aware.
That’s all I can give you for now.
To do it successfully on multi-platform (mine has to work from Vista to Windows-Current) the only really safe bet is to write a wrapper DLL in C++ that can determine at runtime which OS it is on and change code path accordingly. The reason you want to do it in C++ is to avoid passing IAccessible objects across the .Net/unmanaged marshalling boundary. You can call IUnknown::Release on objects you don’t need to return n the unmanaged side. You can do it all in .Net, but it will be slow.
P.S. also watch out for Chrome returning infinite trees where parents are children of their parents, some snity checks are required. Also, Chrome does not return accRole correctly, and will give you HTML tags instead of VT_I4.
Good luck
A fairly workable solution is as follows, in your IAccessible recursive function:
Use getwindowrect to capture the physical right on main window
Use accChild.accLocation in loop to capture left and Width on each Object
Add this simple test
If l > rct2r.Right And l > arrIACC.x2 Then
arrIACC.x2 = l + w
End If
if dpi = 100 then no Object is furter out than physical right
if dpi > 100 then closebutton is...x pix offset
Use the difference to rescale all values you are in use of Width
arrIACC.w1 = CInt(((-rct2r.Left + arrIACC.w1) / arrIACC.x2) * rct2r.Right)
This solution is from an Excel plugin I have developed, I was testing the Width of the quick access toolbar qat and my result was +- 5 pixels regardless of any DPI.

How to measure static size beforehand? WINAPI

I am creating a widow with static text, and because of the all 96/120/180 DPI stuff, I need to create a layouting mini-engine.
The dialog is created in code, statics are created in code, fonts are created in code, everything, mostly because resources in .rc have their share of DPI related problems as well and I want a total control.
The problem with all this is that I don't know how to find the length of the text in statics. I need to calculate the initial size of the static control, and also, I need to calculate a padding between different statics in font unit sizes, but since I don't know the size of the previous static, I can't offset the next one.
The biggest problem is that static does the word wrapping, therefore I can't find a text measuring function that would calculate that and a correction for a custom font, italic, bold, oversize...
Anyone have any ideas?
The static control styles (ENDELLIPSIS,PATHELLIPSIS and LEFTNOWORDWRAP) seem to map to the DrawText flags, so calling DrawText with DT_WORDBREAK|DT_CALCRECT will probably be as close as you can get...
I can't think of any compelling reason to do this any differently then the way all other GUI class libraries do it. Just scale window sizes between the 'design' DPI setting and the target machine DPI setting. Using DPI-independent constants is pretty painful in MFC since everything is pixel based. So keep your workstation at the common 96 DPI setting, scale from there on the target machine. You do have to keep a bit of slack because of TrueType hinting.

About DPI issue

I have a WIN32 SW which the UI was designed in 96 DPI, so when user changes the windows DPI from 96 to 120 or bigger, the UI will be wrong. I want to know if there is API to force my SW to display the UI with 96DPI.
Starting with Windows Vista, scaling for DPI is supposed to happen automatically. I don't have any direct experience to know how well it works, but here's the page that explains how to turn it off:
http://msdn.microsoft.com/en-us/library/ms701681(VS.85).aspx
You can also add an appcompat key for your application. The place for this in the registry is:
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers
That is the per-user settings, there is the same key in HKEY_LOCAL_MACHINE, but of course that is a system setting and will require elevated privileges to write to. Adding a key like so:
"C:\path\to\app.exe"="HIGHDPIAWARE"
Will enable that compatibility flag for your program, which will turn off DPI scaling. This is for Vista+.
SetProcessDPIAware is also an option, but be aware there is a danger of a race condition, according to the documentation.
There is no API to force your app to show at 96DPI. The DPI is a device setting and cannot be controlled per application.
If you can change your program, you can scale your UI to look properly on high DPI though. You need to call GetDeviceCaps; more specificaly, you need to calculate the X and Y scale using the number returned for LOGPIXELSX and LOGPIXELSY. Something like this:
HDC hdc;
double m_dDPIScaleX = GetDeviceCaps(hdc, LOGPIXELSX) / 96.0;
double m_dDPIScaleY = GetDeviceCaps(hdc, LOGPIXELSY) / 96.0;

Resources