dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.05k stars 1.17k forks source link

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

Open Perpete opened 4 years ago

Perpete commented 4 years ago

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](https://user-images.githubusercontent.com/66659162/84110365-06a1cb80-aa25-11ea-80bd-e7cbac59e4e5.png) [WpfApp2.zip](https://github.com/dotnet/wpf/files/4751287/WpfApp2.zip)
lindexi commented 4 years ago

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

Perpete commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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

Perpete commented 4 years ago

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 commented 4 years ago

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_CONTEXTof 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 commented 4 years ago

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 commented 4 years ago
Perpete commented 4 years ago

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 commented 3 months ago

hi sorry ,does it work now?

lindexi commented 3 months ago

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

xmaxrayx commented 3 months ago

@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 commented 3 months ago

@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 commented 3 months ago

@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 commented 3 months ago

@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 commented 3 months ago

@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 commented 3 months ago

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

lindexi commented 2 months ago

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