Skip to content

Commit 06a543a

Browse files
CopilotBornToBeRoot
andcommitted
DPI fix: detach/reattach embedded window so Windows delivers WM_DPICHANGED natively
Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
1 parent 98f6172 commit 06a543a

File tree

3 files changed

+81
-24
lines changed

3 files changed

+81
-24
lines changed

Source/NETworkManager.Utilities/NativeMethods.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ public enum WindowShowStyle : uint
3939

4040
public enum WM : uint
4141
{
42-
SYSCOMMAND = 0x0112,
43-
DPICHANGED = 0x02E0
42+
SYSCOMMAND = 0x0112
4443
}
4544

4645
#endregion

Source/NETworkManager/Controls/PowerShellControl.xaml.cs

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,54 @@ public partial class PowerShellControl : UserControlBase, IDragablzTabItem, IEmb
1616
{
1717
#region Events
1818

19+
private bool _isDpiChanging;
20+
1921
private void WindowGrid_SizeChanged(object sender, SizeChangedEventArgs e)
2022
{
2123
ResizeEmbeddedWindow();
2224
}
2325

24-
private void WindowsFormsHost_DpiChanged(object sender, DpiChangedEventArgs e)
26+
private async void WindowsFormsHost_DpiChanged(object sender, DpiChangedEventArgs e)
2527
{
26-
// Resize first so the embedded window is physically on the new monitor,
27-
// then post WM_DPICHANGED with the explicit new DPI in wParam so the
28-
// embedded process rescales its fonts without relying on a cross-process
29-
// GetDpiForWindow() call (which is unreliable after SetParent across processes).
30-
ResizeEmbeddedWindow();
28+
if (!IsConnected || _appWin == IntPtr.Zero || _isDpiChanging)
29+
return;
3130

32-
if (IsConnected && _appWin != IntPtr.Zero)
31+
_isDpiChanging = true;
32+
33+
try
34+
{
35+
// Windows does not forward DPI change notifications across process boundaries
36+
// after SetParent. Workaround: temporarily detach the embedded window back to
37+
// a top-level window on the new monitor. Windows then delivers WM_DPICHANGED
38+
// natively to the process's own message loop so it can rescale its fonts,
39+
// exactly as it would when running standalone. Then re-embed and resize.
40+
var screen = System.Windows.Forms.Screen.FromHandle(WindowHost.Handle);
41+
42+
NativeMethods.ShowWindow(_appWin, NativeMethods.WindowShowStyle.Hide);
43+
NativeMethods.SetParent(_appWin, IntPtr.Zero);
44+
45+
// Place the window on the new monitor so Windows sends it WM_DPICHANGED.
46+
// The size (800x600) is a reasonable placeholder; the final dimensions are
47+
// set by ResizeEmbeddedWindow() after re-embedding.
48+
NativeMethods.SetWindowPos(_appWin, IntPtr.Zero,
49+
screen.Bounds.X + screen.Bounds.Width / 2,
50+
screen.Bounds.Y + screen.Bounds.Height / 2,
51+
800, 600, NativeMethods.SWP_NOZORDER | NativeMethods.SWP_NOACTIVATE);
52+
53+
// 250 ms gives the process's message loop time to dequeue and handle
54+
// WM_DPICHANGED, which Windows delivers asynchronously for cross-thread windows.
55+
await Task.Delay(250);
56+
57+
if (!IsConnected || _appWin == IntPtr.Zero)
58+
return;
59+
60+
NativeMethods.SetParent(_appWin, WindowHost.Handle);
61+
ResizeEmbeddedWindow();
62+
NativeMethods.ShowWindow(_appWin, NativeMethods.WindowShowStyle.ShowNoActivate);
63+
}
64+
finally
3365
{
34-
var dpiX = (int)e.NewDpi.PixelsPerInchX;
35-
var dpiY = (int)e.NewDpi.PixelsPerInchY;
36-
var wParam = new IntPtr(unchecked((int)(uint)((dpiX & 0xFFFF) | ((dpiY & 0xFFFF) << 16))));
37-
NativeMethods.PostMessage(_appWin, (uint)NativeMethods.WM.DPICHANGED, wParam, IntPtr.Zero);
66+
_isDpiChanging = false;
3867
}
3968
}
4069

Source/NETworkManager/Controls/PuTTYControl.xaml.cs

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,54 @@ public partial class PuTTYControl : UserControlBase, IDragablzTabItem, IEmbedded
1717
{
1818
#region Events
1919

20+
private bool _isDpiChanging;
21+
2022
private void WindowGrid_SizeChanged(object sender, SizeChangedEventArgs e)
2123
{
2224
ResizeEmbeddedWindow();
2325
}
2426

25-
private void WindowsFormsHost_DpiChanged(object sender, DpiChangedEventArgs e)
27+
private async void WindowsFormsHost_DpiChanged(object sender, DpiChangedEventArgs e)
2628
{
27-
// Resize first so the embedded window is physically on the new monitor,
28-
// then post WM_DPICHANGED with the explicit new DPI in wParam so the
29-
// embedded process rescales its fonts without relying on a cross-process
30-
// GetDpiForWindow() call (which is unreliable after SetParent across processes).
31-
ResizeEmbeddedWindow();
29+
if (!IsConnected || _appWin == IntPtr.Zero || _isDpiChanging)
30+
return;
3231

33-
if (IsConnected && _appWin != IntPtr.Zero)
32+
_isDpiChanging = true;
33+
34+
try
35+
{
36+
// Windows does not forward DPI change notifications across process boundaries
37+
// after SetParent. Workaround: temporarily detach the embedded window back to
38+
// a top-level window on the new monitor. Windows then delivers WM_DPICHANGED
39+
// natively to the process's own message loop so it can rescale its fonts,
40+
// exactly as it would when running standalone. Then re-embed and resize.
41+
var screen = System.Windows.Forms.Screen.FromHandle(WindowHost.Handle);
42+
43+
NativeMethods.ShowWindow(_appWin, NativeMethods.WindowShowStyle.Hide);
44+
NativeMethods.SetParent(_appWin, IntPtr.Zero);
45+
46+
// Place the window on the new monitor so Windows sends it WM_DPICHANGED.
47+
// The size (800x600) is a reasonable placeholder; the final dimensions are
48+
// set by ResizeEmbeddedWindow() after re-embedding.
49+
NativeMethods.SetWindowPos(_appWin, IntPtr.Zero,
50+
screen.Bounds.X + screen.Bounds.Width / 2,
51+
screen.Bounds.Y + screen.Bounds.Height / 2,
52+
800, 600, NativeMethods.SWP_NOZORDER | NativeMethods.SWP_NOACTIVATE);
53+
54+
// 250 ms gives the process's message loop time to dequeue and handle
55+
// WM_DPICHANGED, which Windows delivers asynchronously for cross-thread windows.
56+
await Task.Delay(250);
57+
58+
if (!IsConnected || _appWin == IntPtr.Zero)
59+
return;
60+
61+
NativeMethods.SetParent(_appWin, WindowHost.Handle);
62+
ResizeEmbeddedWindow();
63+
NativeMethods.ShowWindow(_appWin, NativeMethods.WindowShowStyle.ShowNoActivate);
64+
}
65+
finally
3466
{
35-
var dpiX = (int)e.NewDpi.PixelsPerInchX;
36-
var dpiY = (int)e.NewDpi.PixelsPerInchY;
37-
var wParam = new IntPtr(unchecked((int)(uint)((dpiX & 0xFFFF) | ((dpiY & 0xFFFF) << 16))));
38-
NativeMethods.PostMessage(_appWin, (uint)NativeMethods.WM.DPICHANGED, wParam, IntPtr.Zero);
67+
_isDpiChanging = false;
3968
}
4069
}
4170

0 commit comments

Comments
 (0)