dotnet / wpf

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

WM_DPICHANGED - WM_WINDOWPOSCHANGING - location windows WPF #3343

Open Perpete opened 4 years ago

Perpete commented 4 years ago

Hello, In a WPF project, I need to position different windows on multiple screens using dpi awarness in PerMonitorV2 mode. After several tests with Framework 4.8 or Core 3.1, I noticed a problem with the positioning values of the windows. So, I wrote a little test program to retrieve some messages sent to my window using a WndProc function to check where the problem was. For the test, I move my window between 2 monitors with different scales. If I move around the monitor at 100% scale, in the image below, you can see that my window position values correctly match the position values given by the WM_WINDOWPOSCHANGING message.

Before

When deciding to switch my window to the monitor at 150% scale, I receive the scale change message (WM_DPICHANGED). In the image below, you can see the suggested position and size for the window. The scale goes to 150% The size values are multiplied by 1.5. Height: 500x1.5 = 750 Width: 800x1.5 = 1200 Position values remain constant

The WM_WINDOWPOSCHANGING message confirms the acceptance of this suggested size and position. After this change of scale, I see that the values for the size of my window have been divided by 1.5 compared to the suggestion in order to keep these values constant. Me.height: 750 / 1.5 = 500 Me.width: 1200 / 1.5 = 800

On the other hand, the position values of my window were also divided by 1.5. Me.top: 91 / 1.5 = 61 Me.left: 1212 / 1.5 = 808

After

If I continue to move my window on the monitor whose scale is 150%, you can see that the WM_WINDOWPOSCHANGING message confirms the position in logical units while the position values of my window are still divided by the scale of the monitor.

Move

My window positioning values are logical units (96dpi). They should not be divided by the monitor scale. Is the problem related to Framework 4.8 or Core 3.1? You can perform the test using the program contained in the zip file.

WpfMessageWindow.zip

vatsan-madhavan commented 4 years ago

My window positioning values are logical units (96dpi).

I don't understand this assertion. Specifically, I don't understand what the phrase "My window positioning values' refers to. Does it refer to WM_WINDOWPOCHANGING's lParam payload, or does it refer to (Window.Left, Window.Top) (or something else) ?

you can see that the WM_WINDOWPOSCHANGING message confirms the position in logical units

What makes you think that WM_WINDOWPOSCHANGING's lParam payload carries DPI-unaware coordinates scaled to 96-dpi space (like WPF's (Window.Left, Window.Top)) in DPI-aware applications) ?

Perpete commented 4 years ago

Hello,

The window positioning values ​​are the values ​​of the Window.Top and window.left properties of the windows. According to the microsoft documentation, these values ​​are in logical units (1/96th of an inch).

https://docs.microsoft.com/en-us/dotnet/api/system.windows.window.left?view=netcore-3.1

These values ​​are read and displayed after moving my window in the Window properties part.

There is indeed a recognition of scaling. The parameters of the WM_DPICHANGED message are taken into account because they are found in the WM_WINDOWPOSCHANGING message.

The parameters of the WM_WINDOWPOSCHANGING message indicate the correct position of the window on the screen and my window is in this position. I check this using another program that gives me mouse coordinates using MOUSEHOOKSTRUCT / SetWindowsHookEx.

I only think the Window.Top and window.left values ​​of the window are incorrect. They should be the same as the parameter position values ​​of the WM_WINDOWPOSCHANGING message to remain in logical units and not divided by the monitor scale.

Perpete commented 4 years ago

I also performed the tests without Per Monitor V2 in WPF and WinForm. When I change the scale of the monitor my window is on, the position values of the WM_WINDOWPOSCHANGING message are divided by the monitor scale. The window.top and window.left values of the window follow the parameter values of this message.

Also in this case, the window.top and window.left values are no longer in logical units.

Monitor with 100% scale 100pr

Monitor with 150% scale Window.left changed from 400 to 267 (400/1.5 =267 150pc )

miloush commented 4 years ago

Not sure I see the problem, as you found in the documentation, the WPF units are logical, while WinForms are pixels. It seems to be working as documented.

My window positioning values are logical units (96dpi). They should not be divided by the monitor scale.

If your values are logical units, then just set the WPF Top/Left properties. They need to be transformed to the pixels for the native API.

Perpete commented 4 years ago

Hello, To explain my point more clearly, here is an image of a screen of a window in Winform and a window in WPF and the performance of a test. Each window displays its Left property value using the window move event.

I place the 2 windows in the same position on the left side (Example illustrated 1000) with the monitor at 100% scale. Then I change the monitor scale to 150%, I visualize the following things:

I think that the values of the top and left properties of the windows should be kept independent of the monitor scale. They should therefore not be divided. ChangeScale

miloush commented 4 years ago

Oh so you are unhappy with the design decision of scaling the Top/Left properties rather than reporting a bug, correct?

I am afraid that regardless of the design, changing the way it works at this moment would be a breaking change to existing applications.

Perpete commented 4 years ago

Hello, To complete the information about Window.left and Window.top values in PerMonitorV2 mode with WPF, I'll show you another test. I wrote a small test program to perform the examples below.

RequestMove

I put a value in a textbox to position the left side of my window. After pressing the Move button, I assign the value of the textbox to the value of the Left property of the window. Then I get the window positioning and size values to display them.

1st example: Screen 1 (Main): 1680x1050 - 100% Screen 2: 1600x900 - 150% Let's position the upper left corner of the window in the center of screen 2. Before moving the window, we assign textbox.value = 2480 -> (1680+ (1600/2)) Once the window has moved, Me.Left = 1653 -> 2480 /1.5

2nd example: Screen 1 (Main): 1680x1050 - 150% Screen 2: 1600x900 - 100% Let's position the upper left corner of the window in the center of screen 2. Before moving the window, we assign textbox.value = 1653 -> (1680+ (1600/2)) /1.5 Once the window has moved, Me.Left = 2480 -> 1653x1.5

3rd example: Screen 1 (Main): 1680x1050 - 125% Screen 2: 1600x900 - 150% Let's position the upper left corner of the window in the center of screen 2. Before moving the window, we assign textbox.value = 1977 -> (1680+ (1600/2)) /1.25 Once the window has moved, This.Left = 1647 -> (1977x1.25) / 1. 5

From these 3 examples, we can conclude:

In this positioning management, we no longer keep the window.top and window.left values in logical units (1 / 96th of an inch) as described in the documentation. However, in the WM_WINDOWPOSCHANGING messages coming from a SetWindowPos sent to my window, the values are indeed in logical units (See my previous comments). When moving between monitors, we do not keep the initial value of the positioning (window.left and textbox.value values are different).

At this time, I don't know whether to consider this handling of window.left and top values as a bug. In any case, these values should be in logical units as described in the documentation, which would simplify the management of the values assigned for a positioning of windows in PerMonitorV2 mode.

From a WPF program, I created windows on the desktop from a mouse selection area and memorized the coordinates of the created windows so they could be repositioned when the program opened again. I would like this program to work with several monitors that have different scales as well. Therefore, I am considering using PerMonitorV2 mode. Before making any changes to my program and from the results of my tests, I would like to know if the current handling of the window.left and top values is correct.

Currently, I would like a response from a Github member affected by this issue.

vatsan-madhavan commented 4 years ago

wParam/lParam payload in window messages like WM_WINDOWPOSCHANGING that contain coordinates/points are expected to be automatically scaled by Windows (the scaling reference would be the receiving threads DPI_AWARENESS_CONTEXT). This means that a PerMonitorV2 (PMAv2) application would receive PMAv2 coordinates.

Window.Left/Window.Top is expected to be expressed in WPF's 1/96" logical coordinate space. The coordinates for the Desktop window is scaled as PMAv2 (on Win10). Imagine transforming the Desktop coordinates from PMAv2 to Unaware (i.e., 1/96" coordinate space) system - this transformed screen space the logical monitor-space onto which Window.Left/Window.Top maps onto.

mstsc /l is a simple way to find the screen coordinate bounds quickly,

Here is an example of how you should think about this:

The thick-lined rectangles represent monitors in actual screen-coordinates (PMAv2) and the dotted-lined rectangles are the same monitors represented in WPF's 1/96th" (i.e., unaware) coordinates). The Windows.Left/Windows.Top values live in the latter space.

download

To make use of the Window.Left/Window.Top values in conjunction with Win32 functions that require screen coordinates in PMAv2 coordinate-space, these values must be transformed first using PointToScreen.

@miloush is right - this Window.Left/Window.Top behavior is not a bug, this is the intended design. That said, We should definitely consider exposing Window.ScreenLeft/Window.ScreenTop properties to minimize this confusion.

Applications that need to save/restore the app position (for e.g., to ensure that the application is loaded at the same place where the user left it off) should continue to use Window.Left/Window.Top for this purpose; as long as the monitor/display configuration doesn't change, the application will be created at the right location on the desktop. For Win32 interop, the hypothetical Window.ScreenLeft/Window.ScreenTop would come in handy.

Perpete commented 4 years ago

Hello, Thank you for your comments.

For the management of applications that manage the saving and restoring of positions, you say that you should continue to use the values of the Window.Left and Window.Top properties in PerMonitorV2 mode.

There might be a problem on an extended desktop with multiple monitors with different scales if the restoring application is not on the monitor where the window to restore have to be.

Example: Screen 1 (Main): 1680x1050 - 100% Screen 2 : 1600x900 - 150%

I create a window and place the top left corner in the center of monitor 2. Once the window is in place, Me.Left = 1653 (See test in the above comments) So, I save the value 1653. After closing the application and reopening the application, I restore the position to the value Me.Left = 1653. As the application opens on my main screen (scale 100%), the window positioning will be determined from this monitor and the restored window will be positioned at 1102 -> 1653 / 1.5 on monitor 2 (scale 150%). Which is no longer the right position.

Let's take another case: The window is by the corner the left edge of screen 2, Me.Left = 1120 (saved value) After restoring from screen 1, Me.Left = 1120 (restored value). The window is now on Screen 1 instead of Screen 2.

Under these conditions, which positioning values must be saved before restoration?

Save_Restaure1

Regarding the transformation of Window.Top and Window.Left values using PointToScreen, I noticed that the coordinate values of a point take margins into account. Comparing the values of win32 functions and the window.top and window.left, they are not equal. I wonder whether there would be a problem.

Window.Top = 0 -> Point.Y = 31 Window.Left = 0 -> Point.X = 8

PointToScreen

Will the Window.ScreenLeft and Window.ScreenTop properties give the same values as PointToScreen?

vatsan-madhavan commented 4 years ago

Just so your experiments and mine line up, are you trying these on netcoreapp3.1 with the most recent SDK? There are bug-fixes in .NET 4.8 (esp. in relation to this scenario, in fact) that are disabled by default (but happen to be enabled by default in .NET Core 3.1). We can help you figure out how to enable those bug-fixes in .NET 4.8 once you puzzle out the right application behavior, but for the purpose of keeping this conversation simple .netcoreapp3.1 is best.

vatsan-madhavan commented 4 years ago

Here is a simple app that shows how saving/restoring works - https://github.com/scratch-space/RestoreAppPosition. Everything happens in MainWindow.xaml.cs it's pretty straightforward.

The screen-recording is pretty low-res but you can see the app restarts at the same place it was closed.

Simply put, the window should be restored to the exact position without any problems. It shouldn't matter whether you are running in SystemAware or PerMonitorAware(v2) - it should work the same. You don't need to transform Window.Top/Left in any way - just use it as-is.

Perpete commented 4 years ago

Hello,

Based on your feedback, I downloaded the RestoreAppPosition app and installed the latest .NET Core 3.1 version (Visual Studio 2019 SDK v3.1.401). After that, I placed the RestoreAppPosition.exe shortcut in the taskbar. The shortcut is accessible in the 2 task bars below each monitor.

On my extended desktop, if I perform the backup and restore operation from the same monitor using the application shortcut on that monitor's taskbar, it works fine. On the other hand, if I perform the backup operation from the 2nd monitor and the restore from the 1st monitor using the shortcut on the taskbars of the 1st monitor, it no longer works.

Here is the example: Monitor 1 (1680x1050 - 100%) Monitor 2 (1600x900 - 150%)

Starting the RestoreAppPosition application from the monitor taskbar 2. Positioning of window on monitor 2 (1600x900 - 150%) Then close this window

Save

Restart the RestoreAppPosition application from the monitor 1 taskbar (1680x1050 - 100%). The restore position is no longer correct, the left and right values have been divided by the scale of monitor 2. Left : 1300 / 1.5= 866 Top : 75 / 1.5= 50

Restaure

In this example, we have access to the 2 shortcuts on the task bars.

In my window manager application, the program opens from the main screen. So, I'm going to meet the same situation of having to restore windows to monitor 2 from monitor 1 and face the positioning issue described in my comments.

Under these conditions, shouldn't we memorize the coordinates / points and convert the Top and Left values into coordinates / points?

Example 1: Monitor 1 (1680x1050 - 100%) Monitor 2 (1600x900 - 150%)

Left: 1300 on monitor 2 Save: 1300x1.5 = 1950 (coordinates / points) Restore from monitor 1 Left: 1950/1 = 1950

Example 2: Monitor 1 (1680x1050 - 125%) Monitor 2 (1600x900 - 150%)

Left: 1300 on monitor 2 Save: 1300x1.5 = 1950 (coordinates / points) Restore from monitor 1 Left: 1950 / 1.25 = 1560

From these above examples and after some tests, the restoration of the window is correct.

From the example of your RestoreAppPosition app, I created another app that you can test (WpfRestoreSave.zip). This application works as explained in examples 1 and 2 above. The application restores correctly the windows on all monitors from any other monitors regardless of the scales.

WpfRestoreSave.zip

vatsan-madhavan commented 4 years ago

~A quick question about this:~

~Starting the RestoreAppPosition application from the monitor taskbar 2.~ ~Positioning of window on monitor 2 (1600x900 - 150%)~ ~Then close this window~ ~~

~Is your application straddling the two monitors - i.e., positioned in such a way that it is partly within one monitor and partly within another monitor (instead of being fully contained inside one monitor) ?~

Edit: I think I understand what you're describing now. That shouldn't happen - the pinned taskbar icons aren't DPI or monitor sensitive, at least not that I know of. What version of Windows 10 are you running - I'd like to try this out on the same version of Win10 as yours. I want to understand if there is a bug (WPF or Windows) or insufficiency (in WPF) etc. that's preventing WPF from achieving this goal. First step would be to reproduce the problem.

Re your approach: You can get the DPI more easily via VisualTreeHelper.GetDpi. The DPI along both x and y axes will always be identical. In more complex cases, this approach suffers from WM_DPICHANGED events being delivered to the window at creation time, which can in turn result in (sometimes expensive) re-layout. For e.g., if the Window had been created at (0,0) or another position at 100% DPI, and you were to move it to a different position at 200% DPI, it would receive a DPI changed event, go through layout change etc. If there is a context-menu or a popup in this path, it would almost certainly not adapt properly (because there are weaknesses/bugs in the layout heuristics associated with them). That said, this might very well work for your case if the complexity and the elements in the application happen to be compatible with this type of one-off re-layout at startup. You'll know for sure when testing.

The in-built logic for Window.Top/Left sidesteps these concerns by creating the HwndSource (i.e., the HWND) at the expected position on the screen, and avoids incurring additional DPI-change messages etc.

Perpete commented 4 years ago

Hello,

To clarify my problem, here is a test procedure from your RestoreAppPosition application. With a desktop in extended mode made up of 2 monitors with different scales, you can perform the following test: • Open the RestoreAppPosition application on monitor 1. • Move the window to the monitor 2. • Close the window on monitor 2 (Save the top - left position by the application) • Restart the RestoreAppPosition application on monitor 1 (Restores the top - left position by the application). • The window should be restored to the previous position on monitor 2.

In this test, since the repositioning is not correct, the only current solution I have found is to convert the Top and Left values to coordinates / points and vice versa taking into account the scales of the monitors. (See previous comments). The WpfRestoreSave.zip file contains this application which you can test.

With the current positioning support in WPF, each monitor sees all of the other monitors from the Windows desktop with its own scale. This is the problem of restoring the position of the window.

Example: Monitor 1 (1920x1080 - 100%) Monitor 2 (1920x1080 - 150%) Let’s place the top left corner of a window in the center of monitor 2. The left value considered by monitor 1 = 2880 -> (1920 +1920/2) The left value considered by monitor 2 = 1920 -> (1920 +1920/2) /1.5

Is this handling in WPF the intended design decision to scale top and left properties in PerMonitorV2 mode with multiple monitors for the desktop? If so, my window position backup and restore test program contained in WpfRestoreSave.zip will solve my problem. This test program will allow me to modify another program to make it compatible with PerMonitorV2 mode.

To avoid this problem, the window positioning values could be viewed as physical values for each monitor placed side by side (1920+ (1920 / 1.5) = 2880). The window.Left value from the example above would be: (1920/1) + ((1920/2) /1.5) = 2560. Therefore, WPF should no longer change the placement values based on monitor scales by dividing them.

For your testing, my version of Win10 is as follows: Edition 10 Family 2004 version Operating system version: 19041.450