Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Positioning a window in WPF (Top and Left Properties) with dpiAwareness in PerMonitorV2 mode #3105

Open
Perpete opened this issue Jun 9, 2020 · 20 comments
Milestone

Comments

@Perpete
Copy link

Perpete commented Jun 9, 2020

Hello,

For a WPF project in VB, I retrieve the mouse coordinates using a Hook mouse (MSLLHOOKSTRUCT) to display a window on the desktop according to the selection rectangle defined by the user.
I noticed that the mouse coordinates do not take into account the screen scales.
Is this normal?
So, I had to retrieve this scale using the GetDpiForMonitor API and created a manifest file with DPI recognition per screen.
PerMonitorV2 .
In a test project, I noticed problems with the bad positioning of my window in relation to the placement request.
Here are my tests with the Left property of a window:
Screen resolution 1: 1680x1050
Screen resolution 2: 1600x900

Screen 1 = 100% - Screen 2 = 150%.**
Window on screen 1, asks to switch to screen 2
Ask left = 1680 after moving : left = 1120 on screen 2
The left was divided by 1.5 after the move.

Window on screen 2, asks to switch to screen 1
Ask left = 100 after moving : left = 150 on screen 1
The left was multiplied by 1.5 after the move.

Screen 1 = 150% - Screen 2 = 100%.
Window on screen 1, asks to switch to screen 2
Ask left = 1680 after moving : left = 2520 on screen 2
The left was multiplied by 1.5 after the move.

Window on screen 2, asks to switch to screen 1
Ask left = 100 after moving : left = 66.66 on screen 1
The left was divided by 1.5 after the move.

Is this management of the values of the positioning properties correct?
These requested positions do not correspond to the physical position where I want to place my window.
Without DPI recognition per screen, the positioning of the window respects the requested coordinates. The Top and Left properties do not change despite the different scales.

You can perform the test using the program contained in the zip file.

Left_Window

WpfApp2.zip

@lindexi
Copy link
Member

lindexi commented Jun 10, 2020

You should make sure your system version later than Windows 10 Anniversary Update

@Perpete
Copy link
Author

Perpete commented Jun 10, 2020

My version of windows 10 is 1909.
My Visual Studio version is 16.6.1.
The FrameWork 4.7.2 and 4.8 reproduce the same problem.
I have tried my test program on other PCs with other screens. I also have the same problem.
I found this link to a problem similar to mine open in 2018 and stay open.
https://github.com/QL-Win/QuickLook/issues/420
It is the same problem for the show method or the LocationChanged event, the top and left values are adapted after the event.
There is a discrepancy between the logical coordinates given by the Hook mouse procedure and the coordinates reassessed after moving and displaying (physical coordinates) with dpi recognition per monitor.
On the other hand after other tests, I noticed that the values of the Height and Width properties of the window remain constant after switching from one monitor to another with different scales.

@Perpete
Copy link
Author

Perpete commented Jun 20, 2020

Hello,
Using a program inspecting system messages sent to windows, I found the following for a window move in WPF with dpiAwareness in PerMonitorV2 mode.
Screen 1: 1680x1050 - 100% scale
Screen 2: 1600x900 - Scale 150%

Window: Height = 492 - Width = 509

Test 1
Window on screen 1, asks to go to screen 2 at the position Me.Left = 1680 and Me.Top = 0.

Move1680

I note that the message WM_DPICHANGED (0x02E0) was sent to my window with 144dpi (0x90) for X and 144dpi (0x90) for Y. (150%)
Message WM_WINDOWPOSCHANGING shows the new size and position of the window about to change.
As the scale on screen 2 is 150%, Me.Width becomes 509x1.5 = 764, Me.Height becomes 409x1.5 = 738. On the other hand X remains at 1680.

After moving the window, the properties have the following values:
Me.Top = 0
Me.Left = 1120
Me.Width = 509
Me.Height = 492
All values in the window have been divided by 1.5.

Move1680Wd

Test 2
Window on screen 2, request to go to screen 1 at the position Me.Left = 100 and Me.Top = 0.

Move100

I note that the message WM_DPICHANGED (0x02E0) was sent to my window with 96dpi (0x60) for X and 96dpi (0x60) for Y. (100%)
Message WM_WINDOWPOSCHANGING shows the new size and position of the window about to change.
As the scale on screen 1 is 100%, Me.Width and Me.Height keep their value (509-492)
On the other hand X goes to 150 while Me.Left = 100.

After moving the window, the properties have the following values:
Me.Top = 0
Me.Left = 150
Me.Width = 509
Me.Height = 492
The values ​​in the window have not been changed.

Move100Wd

I think there is a problem with positioning the window on an extended desk.
The positioning values are adapted according to the scale of the monitor where the window is located before it is moved, while the values of the size are linked to the scale of the destination monitor.
Giving a window position in this way is disturbing for the user.

The positioning values are logical units (96dpi).
Should we not keep these values without modifying them so that the user can set a clear positioning value?

Can this change be made in the future because I need to use the positioning values of different windows in a WPF program on several monitors with different scales?

@vatsan-madhavan
Copy link
Member

Try this on netcoreapp3.1 for best experimental results, esp since most AppContext switches don’t have to be set for this TFM.

Make sure you have "Switch.System.Windows.DoNotUsePresentationDpiCapabilityTier2OrGreater" to false.

IIRC Window.Left/Top is expressed in WPF’s local device independent 1/96” coordinate space. It must be translated to screen coordinate using Visual.PointToScreen. It’s no different that translating any point in a Visual from WPF to screen coordinates (or vice-versa).

@fabiant3
Copy link
Member

@Perpete - please let us know if @vatsan-madhavan suggestions is working for you.

@Perpete
Copy link
Author

Perpete commented Jun 23, 2020

Hello,

To use Core 3.1, I had to convert my written test program from VB to C #.
I carried out the same tests and the problem of managing the positioning of the window remains the same.

You can use my test program contained in the .zip file.
It was created with Visual Studio 2019 Version 16.6.1 with core 3.1.
The.exe file is located in the folder:
WpfTestPositionCore.zip \ WpfTestPositionCore \ WpfTestPosition \ bin \ Debug \ netcoreapp3.1.

Thanks for your help.

WpfTestPositionCore.zip

@vatsan-madhavan
Copy link
Member

vatsan-madhavan commented Jun 23, 2020

What I tried to explain in my last comment simply related to interpreting Window.Left/Top in relation to screen coordinates - that it follows certain rules (they are expressed in WPF's device-independent coordinate space, and not in screen-coordinates), and application developers must adapt to this reality by leveraging transformation routines provided by WPF (specifically, converting from WPF-space to screen-space can be accomplished by leveraging Visual.PointToScreen etc.).

If you search for information regarding the reverse transformation (screen -> logical), you'll find plenty of references about how to do that. (Roughly, something like PresentationSource.FromVisual(window).CompositionTarget.TransformFromDevice.Transform(point))

Note that these transformations aren't static - don't try cache them esp. They depend on the current DPI, current position on the screen etc. Just rely on WPF to produce these transformations on-the-fly.

You can generalize this information to further debug your problems with MSLLHOOKSTRUCT. Payload that comes from Win32 API's is outside of WPF's control. You can attempt to identify the semantics of Win32 API's based on their documentation, and supplement with experimentation.

When per-monitor dpi-awareness is involved, a critical piece of information that's needed wrt Win32 API's (esp. API's that carry coordinate information in their data-payload) is whether Windows auto-scales the coordinates (in the data-payload) to match the DPI_AWARENESS_CONTEXT of the thread receiving the message. Windows has made significant effort to ensure that this is indeed the case. For e.g., a DPI_AWARENESS_CONTEXT_SYSTEM_AWARE application would typically receive WM_MOVE messages with lParam payload (x,y) values scaled as if the universe were entirely system-aware. This means that the application receiving this message can simply use these values without concerning itself with further transformations/scaling.

That said, some API's may not do this - either by design, or because they have not been updated. This is why I'm suggesting that you'll have to combine perusing documentation with experimental observations. In the case of MSLLHOOKSTRUCT, the doc clearly states that the data is per-monitor-aware (which I interpret as "the coordinates are always reported in DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, irrespective of the receiving applications DPI_AWARENESS_CONTEXT" - I wish this had been stated more clearly in the docs).

Have you considered using MOUSEHOOKSTRUCT/SetWindowsHookEx instead? I realize that this isn't suitable for all needs.

Also be aware that the "screen-coordinates" are essentially DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE[_V2] space coordinates of the desktop window.

@Perpete Perpete closed this as completed Jun 24, 2020
@Perpete Perpete reopened this Jun 24, 2020
@Perpete
Copy link
Author

Perpete commented Jun 24, 2020

Hello,

In my test program, I simply request the displacement of the window with the property Me.Left = XXX whose value is entered in a TextBox.
The value of the property displayed by the textbox is similar to the data provided by MOUSEHOOKSTRUCT / SetWindowsHookEx representing the coordinates of all the monitors not sensitive to their scale.
I also display the coordinates of the mouse using MOUSEHOOKSTRUCT / SetWindowsHookEx in my test program.
The problem for me is that the requested value is sensitive to the scale of the monitor where the window is located before it is moved.

If I am on the 2nd monitor which has a scale 150% and I want to move the window on the 1st monitor which has a scale 100% at the value Left = 100 (96dpi), I must set Me.Left = 66.66 (100 /1.5=66.66).

If I am on the 1st monitor which has a 100% scale and I want to move the window on the 2nd monitor which has a 150% scale in the center of it at the value Left = 2880 (96dpi), I must place Me .Left = 2880 (2880/1 = 2880).

If I am on the 1st monitor which has a 125% scale and I want to move the window on the 2nd monitor which has a 150% scale in the center of it at the value Left = 2880 (96dpi), I must place Me .Left = 2304 (2880 / 1.25 = 2304).

Managing the position with dpiAwareness in PerMonitorV2 mode is much more complicated due to the transformation of the Top and Left properties of the window from the monitor scale.

I think that the most easily usable positioning data are the resolutions of the monitors at 96dpi (1920x1080, 1680x1050, ... ...)
To manage the positioning of monitors by scale, you must therefore know the scale of each monitor in order to easily and correctly place its window.

I'm also going to have another problem with managing the window positioning in a program where the window coordinates are saved and where these windows must be repositioned when this program is opened.
It will be necessary to memorize the scale of the monitors to restore the correct positioning of these windows.

My starting question was to know if this management of the position with dpiAwareness in PerMonitorV2 mode of the windows on a extended desktop was correct and frozen in order to be able to start modifying my programs.

Thanks for your help.

@vatsan-madhavan
Copy link
Member

  • An application does not have to be aware of monitor-specific DPI's for any of this. PointToScreen and TransformFromDevice know everything about these characteristics and abstract away the need for such knowledge.
  • Starting .NET 4.8, saving and reinitializing Window.Top/Left in WPF's device-independent coordinates on a multi-mon PMAv2 application will work.
    • This of course reasonably assumes that the geometry - arrangement, resolution, and DPI - of the monitors hasn't changed since the program saved the points (to an external store of its own choice) and exited, and subsequently attempts to reload those saved points. If the underlying monitor-arrangement geometry changes, then the outcome could be unpredictable.
    • The program doesn't have to transform these points in any way before saving them, and perform a reverse transformation prior to restoring them. The idea is to save and restore the Window.Top/Left values as-is and have them just work. WPF does the heavy lifting for you.
    • In .NET Framework, the application must set the appropriate AppContext switches to opt-into these high DPI specific enhancements. Notably, setting "Switch.System.Windows.DoNotUsePresentationDpiCapabilityTier2OrGreater" to false.. This especially matters if the TargetFramework of the application is lower than .NET 4.8. Irrespective of the applications TargetFramework, these DPI-specific enhancements are only available on .NET Framework if the underlying runtime is .NET 4.8.
    • On .NET Framework, I won't recommend attempting to use PMAv2 on anything less than .NET 4.8 runtime. It's ok to target the application (TargetFramework) to something lower (alongwith appropriate AppContext switches to light-up the newer 4.8 specific functionality). But 4.7.2 and older runtimes simply do not have adequate support for PMAv2.
      • On .NET Framework, another notable AppContext switch an app might want to set is Switch.System.Windows.DoNotScaleForDpiChanges=false

@fabiant3 fabiant3 added this to the Future milestone Jun 24, 2020
@Perpete
Copy link
Author

Perpete commented Jun 26, 2020

Hello,

You are right, there is no need for programming code for adjusting window sizes with dpiAwareness in PerMonitorV2 mode.

My test program works very well with Core 3.1, FrameWork 4.8 and FrameWork 4.7.2.
The size of my window adapts correctly to the scale of the monitors with clearly readable fonts.
But for me, the management of positioning with several screens with different scales is a problem.

As I was explaining to you before, I have a program that stores window size and positioning values ​​and recreates those windows at the size and position stored.
With the current adaptation of positioning in PerMonitorV2 mode, management of positioning with several screens with different scales is more complex.

I will use examples to explain myself better.

Screen 1 (Main): 1680x1050
Screen 2: 1600x900
I position my window on the left edge at the top of the 2nd screen at x = 1680 (96dpi) y = 0 (96dpi)

1st Example
Screen 1: 100%
Screen 2: 150%
Values ​​from my window for WPF after scaling and saved in a file.
This.Top = 0
This.Left = 1120 (1680x1) /1.5
This.Heigh = 492
This.Width = 509

When recreating the window with these values, my new window is found on screen 1 at the position This.Left = 1120 (1120/1) and This.Top = 0 instead of screen 2.
If I want the correct position on screen 2, before positioning, I must assign the value This.Left to: ((1120x1.5) / 1) = 1680.
After positioning the window, WPF gives This.Left = 1120.

The This.Height and This.Width values ​​remain at 509 and 492.

2nd Example
Screen 1: 125%
Screen 2: 150%
Values ​​from my window for WPF after scaling and saved in a file.
This.Top = 0
This.Left = 933 (1680x1.25) /1.5
This.Heigh = 492
This.Width = 509

When recreating the window with these values, my new window is found on screen 1 at the position This.Left = 933 ((1120x1.25) /1.5) and This.Top = 0 instead of screen 2 .
If I want the correct position on screen 2, before positioning, I must assign the value This.Left to: ((1120x1.5) /1.25) = 1344.
After positioning the window, WPF gives This.Left = 1120.

The This.Height and This.Width values ​​remain at 509 and 492.

3rd Example
Screen 1: 150%
Screen 2: 100%
Values ​​from my window for WPF after scaling and saved in a file.
This.Top = 0
This.Left = 1680 (1680x1.5) / 1
This.Heigh = 492
This.Width = 509

When recreating the window with these values, my new window is found on screen 2 at the position This.Left = 2520 (1680x1.5) instead of 1680 and This.Top = 0.
If I want the correct position on screen 2, before positioning, I must assign the value This.Left to: ((1680x1) / 1.5) = 1120.
After positioning the window, WPF gives This.Left = 1120.

The This.Height and This.Width values ​​remain at 509 and 492.

To find the value on the scale of the correct position of the window on all screens, I simply apply the opposite formula used between Windows 10 and WPF from the initial value.
You can see the procedure between windows 10 and WPF by the system messages in my previous comments.
The initial position is first multiplied by the scale of the start screen and then divided by the scale of the destination screen.
Obviously, for different scales between screens, with displacements between screens, this value is not constant.

It is for these reasons that I need to know the scale of the screens to restore or move the windows to the right position.

I think there is a problem with scaling the position between Windows 10 and WPF.

Let's take an example :
Screen 1 (Main): 1680x1050 - 100%
Screen 2: 1600x900 - 150%
Let's position the upper left corner of a window in the center of the screen 2.
Without PerMonitorV2
Before moving This.Left = 2480 -> (1680+ (1600/2))
After moving This.Left = 2480

Currently with PerMonitorV2
Before moving This.Left = 2480 -> (1680+ (1600/2))
After moving This.Left = 1653 -> (2480 x1) /1.5
I think the value of 1653 is not correct.
It should be 2213 -> 1680 + (800 / 1.5).

Let's take another example:
Screen 1 (Main): 1680x1050 - 125%
Screen 2: 1600x900 - 150%
Let's position the upper left corner of a window in the center of the screen 2.
Without PerMonitorV2
Before moving This.Left = 2480 -> (1680+ (1600/2))
After moving This.Left = 2480

Currently with PerMonitorV2
Before moving This.Left = 2480 -> (1680+ (1600/2))
After moving This.Left = 2066 -> (2480 x1.25) /1.5
The window position is incorrect.
To place it correctly, I have to adjust the initial value of This.Left by dividing it by the scale of the main screen
This.Lefts = 1977 -> (1680+ (1600/2)) /1.25
After moving This.Left = 1647

I think the value of 1647 is not correct.
It should be from 1877 -> (1680 / 1.25) + (800 / 1.5).

To correct this problem, a solution would be that the rectangle pointed by lParam in the WM_DPICHANGED message contains a Top and Left value calculated by considering each screen resolution with its separate scale.
With my screen configuration in the example above we would have:
This.Left = 2480 -> Rectangle.Left = 1877 -> (1680 / 1.25) + (800 / 1.5).

At this time, Wpf will receive a correct positioning without any adaptation to be made.

Another solution would be to keep the positioning values in logical units (96dpi) without modification between windows 10 and WPF like the modes without PerMonitorV2.

For size values, the management between Windows 10 and WPF works perfectly well.
The This.Height and This.Width values ​​always remain at 492 and 509 with any screen scale.
If This.Height = 492 before positioning, the message WM_DPICHANGED gives the dpi for x and y of the destination monitor in the example window: 0x90 = 144dpi = 150%, the height proposal for scaling will be 492x1. 5 = 738.
After the position change, WPF gives us This.Height = 492.
The height value is divided by the scale factor of the monitor (1.5).
For the user, this value therefore remains the same regardless of the scale.

@xmaxrayx
Copy link

xmaxrayx commented Jul 4, 2024

hi sorry ,does it work now?

@lindexi
Copy link
Member

lindexi commented Jul 4, 2024

@xmaxrayx Sorry, no. It is a design issues. But you can get the correct postion from win32.

@xmaxrayx
Copy link

xmaxrayx commented Jul 4, 2024

@xmaxrayx Sorry, no. It is a design issues. But you can get the correct position from win32.

@lindexi thanks currently my program get the mouse position from user32.DLL but my WPF program won't show up at that position unless if changed windows DPI
image

if i set 100% the WPF can start under mouse position.
image

@lindexi
Copy link
Member

lindexi commented Jul 4, 2024

@xmaxrayx Yeah, you meet a problem with coordinate system transformation. The GetCursorPos is screen coordinate but the Window.Left and Window.Top is wpf coordinate .

@Perpete
Copy link
Author

Perpete commented Jul 5, 2024

@xmaxrayx
Here is how I move a window by code to work around the positioning issue with PerMonitorV2.
During testing, I noticed that I could consider that each monitor saw all the dimensions of the other monitors with its own scale.
Let's take an example of positioning a window with the following monitors:
Monitor 1 (1920x1080 - 100%)
Monitor 2 (1920x1080 - 150%)

For window positioning references, I always use 100% monitor scale values.
Let's put the upper left corner of the window in the center of the width of the monitor 2.
If my window is initially positioned on monitor 1:
The left value to give to the window will be 2879 -> ((1920 +1920/2) / 1) -1

If my window is initially positioned on monitor 2:
The left value to give to the window will be 1919 -> ((1920 +1920/2) / 1.5) -1

Here is my test code in VB.

`
Class MainWindow
Private Sub btMove_Click(sender As Object, e As RoutedEventArgs) Handles btMove.Click

'Déplace la fenêtre

'Récupère le facteur d'échelle du moniteur
Dim ScaleMonitor As DpiScale = VisualTreeHelper.GetDpi(Me)

Dim CalculLeft As Double = CDbl(txtLeft.Text) / ScaleMonitor.DpiScaleX
Dim CalculTop As Double = CDbl(txtTop.Text) / ScaleMonitor.DpiScaleY

'Attibue les coordonnées à la fenêtre
Left = CalculLeft
Top = CalculTop

lbLeftCalcul.Content = CalculLeft
lbTopCalcul.Content = CalculTop
lbScale.Content = ScaleMonitor.DpiScaleX

lbLeftAfterMove.Content = Left
lbTopAfterMove.Content = Top

End Sub
End Class`

Here is my code for test (wpf in vb with net 6).
MoveWindows.zip

I don't understand why this problem hasn't been fixed all this time.
This seems like a significant problem to me.

@xmaxrayx
Copy link

xmaxrayx commented Jul 6, 2024

@Perpete Hi, many thanks it works now <3
my setup is only one monitor with 150% and yeah the WPF scale up the location , I think this bad approach , if it was width or high then no problem but window location shouldn't be that.

this is my code for c#

        public MainWindow()
        {   
            InitializeComponent();
            DpiScale dpi = VisualTreeHelper.GetDpi(this);
            POINT point;  GetCursorPos(out point);
            this.Top = (point.Y)/dpi.DpiScaleY;
            this.Left = (point.X)/dpi.DpiScaleX;
            
        }

yeah I found it funny winforms don't have that problem with wpf.

@xmaxrayx
Copy link

xmaxrayx commented Jul 6, 2024

@xmaxrayx Yeah, you meet a problem with coordinate system transformation. The GetCursorPos is screen coordinate but the Window.Left and Window.Top is wpf coordinate .

thx, wish if they remove that " wpf transform" for window locations or at least we have option for true manual location , wpf multiply that location with the dpi scale then we need divide it again to revoke it ,so in total we do more process work unnecessary.

@Perpete
Copy link
Author

Perpete commented Jul 6, 2024

You are absolutely right.
I reported this issue in 2020 and it's 2024.
Apparently this problem is not considered important.

@lindexi
Copy link
Member

lindexi commented Jul 9, 2024

I failed to fix this problem three years ago...

@bojan-sala-igt-com
Copy link

This bug causes any logic for saving/restoring window layouts to fail on any multi DPI system since WPF always uses the main display DPI. When windows are created they always pick up the default DPI and don't consider he fact that they may be moved around programatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants