Search code examples
windowswinapicoordinatesdpimultiple-monitors

Converting HIMETRIC to pixels in a multi-DPI environment


My UI framework uses double values for pixel coordinates, which gives me nice fractional virtual coordinates on retina displays. For this I get the mouse coords from Windows in HIMETRIC units. However, I've run into a bug that I can't figure out.

I have a Surface Book 2 (3000x2000 @ 225%) plugged into a dock with two external monitors (1920x1080 @ 100%). If I log into Windows with the dock plugged in (and an external monitor @ 100% set as primary), I receive correct HIMETRIC coordinates, which work on both the external and the retina displays.

However, if I log into Windows while the dock is unplugged, so there is only the retina monitor, then plug in the dock, then run my app — now it receives HIMETRIC coords scaled by the scaling factor of the display which was primary when I logged into Windows.

I have set my app's DPI awareness to PerMonitorV2, which is why I find it so strange that the per-system DPI scale setting is even relevant to my app.

This is the function I use to convert from HIMETRIC:

double fromHimetric(uint i)
{
    return (double)i / 2540 * Monitor::DefaultDPI; 
}

Monitor::DefaultDPI is a const set to 96.

Am I missing something? Do I need to add a system scaling factor to the formula? In that case, how can I find out which monitor was primary when I logged into Windows? Because that seems to stay constant no matter which monitor I move my window to, since I receive HIMETRIC values which are scaled by a value from the past, apparently.

Edit: because multi-monitor stuff is hard to describe over the internet I made a little video showing how this bug manifests for me. https://www.youtube.com/watch?v=pTZiTZFXsc0

Edit 2: just to clarify even further, my app completely respects per-monitor DPI, all my UI is vectored so it scales correctly, I handle WM_DISPLAYCHANGE and WM_DPICHANGED, all that stuff. The pen and touch coordinates work fine in all cases too.

The only thing that breaks if I initially start my computer undocked is that the HIMETRIC coords that I get for the mouse from GetPointerInfo() are scaled by the display scale of the monitor which was primary when the computer was started.

I've worked hard to make sure I handle multi-monitor multi-DPI situations WELL. I'll feel very silly if there's a simple thing that I overlooked which is breaking everything.

Edit 3: Look how big the scrollbars are in Chrome. https://i.sstatic.net/ylOg7.jpg

Or OBS studio: https://i.sstatic.net/AyfWq.jpg

They seem to suffer from similar bugs. Don't know if theirs have anything to do with HIMETRIC, but these elements are rendered differently based on how I start the computer.

Does Win32 define something like a startup monitor?

Edit 4: The reason I'm not using AtlHiMetricToPixel() and PhysicalToLogicalPoint() is that they return integer logical coordinates, and I specifically want doubles in logical space, since my whole UI is vector-based. I do all the scaling myself and it seems to work fine, except in this one case. :(


Solution

  • For anyone that will ever be interested in this, I was given the answer here.

    The DPI of the primary monitor at session logon is at

    HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics | AppliedDPI

    I have tested it and it's always correct depending on how I log in. So in my case my app just has to read this from the registry at startup and then use it to adjust the incoming HIMETRIC coords for the mouse.