Skip to content

Commit fd46a3d

Browse files
committed
Fix: PuTTY DPI issues
1 parent 21a2be8 commit fd46a3d

File tree

2 files changed

+62
-116
lines changed

2 files changed

+62
-116
lines changed

Source/NETworkManager.Utilities/NativeMethods.cs

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ public class NativeMethods
2020
public const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
2121

2222
/// <summary>Places the window at the bottom of the Z order (behind all others).</summary>
23-
public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
23+
public static readonly IntPtr HWND_BOTTOM = new(1);
2424

2525
/// <summary>The value returned by CreateFile on failure.</summary>
26-
public static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
26+
public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
2727

2828
#endregion
2929

@@ -74,15 +74,6 @@ public struct RECT
7474
public int left, top, right, bottom;
7575
}
7676

77-
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
78-
public struct MONITORINFO
79-
{
80-
public int cbSize;
81-
public RECT rcMonitor;
82-
public RECT rcWork;
83-
public uint dwFlags;
84-
}
85-
8677
[StructLayout(LayoutKind.Sequential)]
8778
public struct COORD
8879
{
@@ -105,10 +96,7 @@ public struct CONSOLE_FONT_INFOEX
10596
#endregion
10697

10798
#region Pinvoke/Win32 Methods
108-
109-
[DllImport("user32.dll")]
110-
public static extern IntPtr GetForegroundWindow();
111-
99+
112100
[DllImport("user32.dll", SetLastError = true)]
113101
public static extern long SetParent(IntPtr hWndChild, IntPtr hWndParent);
114102

@@ -136,12 +124,6 @@ public static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, in
136124
[DllImport("user32.dll", CharSet = CharSet.Auto)]
137125
public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
138126

139-
[DllImport("user32.dll", CharSet = CharSet.Auto)]
140-
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
141-
142-
[DllImport("user32.dll", SetLastError = true)]
143-
public static extern bool MoveWindow(IntPtr hWnd, int x, int y, int cx, int cy, bool repaint);
144-
145127
[DllImport("user32.dll")]
146128
public static extern bool ShowWindow(IntPtr hWnd, WindowShowStyle nCmdShow);
147129

@@ -165,11 +147,8 @@ public static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, in
165147
[DllImport("user32.dll")]
166148
public static extern uint GetDpiForWindow(IntPtr hWnd);
167149

168-
[DllImport("user32.dll")]
169-
public static extern IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
170-
171-
[DllImport("user32.dll", CharSet = CharSet.Auto)]
172-
public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
150+
[DllImport("user32.dll", SetLastError = true)]
151+
public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);
173152

174153
[DllImport("kernel32.dll", SetLastError = true)]
175154
public static extern bool AttachConsole(uint dwProcessId);
@@ -194,19 +173,7 @@ private static extern bool SetCurrentConsoleFontEx(IntPtr hConsoleOutput, bool b
194173

195174
#endregion
196175

197-
#region Helpers
198-
199-
/// <summary>
200-
/// Returns the physical-pixel bounding rectangle of the monitor that contains
201-
/// the specified window (nearest monitor if the window is off-screen).
202-
/// </summary>
203-
public static RECT GetMonitorBoundsForWindow(IntPtr hWnd)
204-
{
205-
var hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
206-
var mi = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
207-
GetMonitorInfo(hMonitor, ref mi);
208-
return mi.rcMonitor;
209-
}
176+
#region Helpers
210177

211178
/// <summary>
212179
/// Attaches to <paramref name="processId"/>'s console and rescales its current font
@@ -255,5 +222,41 @@ public static void TryRescaleConsoleFont(uint processId, double scaleFactor)
255222
}
256223
}
257224

225+
/// <summary>
226+
/// Sends a <c>WM_DPICHANGED</c> message to a GUI window (e.g. PuTTY) so it can
227+
/// rescale its fonts and layout internally. This is necessary because
228+
/// <c>WM_DPICHANGED</c> is not reliably forwarded to cross-process child windows
229+
/// embedded via <c>SetParent</c>. Requires PuTTY 0.75+ to take effect.
230+
/// </summary>
231+
public static void TrySendDpiChangedMessage(IntPtr hWnd, double oldDpi, double newDpi)
232+
{
233+
if (hWnd == IntPtr.Zero)
234+
return;
235+
236+
if (Math.Abs(newDpi - oldDpi) < 0.01)
237+
return;
238+
239+
const uint WM_DPICHANGED = 0x02E0;
240+
241+
var newDpiInt = (int)Math.Round(newDpi);
242+
var wParam = (IntPtr)((newDpiInt << 16) | newDpiInt); // HIWORD = Y DPI, LOWORD = X DPI
243+
244+
// Build the suggested new rect from the current window position.
245+
var rect = new RECT();
246+
GetWindowRect(hWnd, ref rect);
247+
248+
// lParam must point to a RECT with the suggested new size/position.
249+
var lParam = Marshal.AllocHGlobal(Marshal.SizeOf<RECT>());
250+
try
251+
{
252+
Marshal.StructureToPtr(rect, lParam, false);
253+
SendMessage(hWnd, WM_DPICHANGED, wParam, lParam);
254+
}
255+
finally
256+
{
257+
Marshal.FreeHGlobal(lParam);
258+
}
259+
}
260+
258261
#endregion
259262
}

Source/NETworkManager/Controls/PuTTYControl.xaml.cs

Lines changed: 20 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -17,85 +17,27 @@ public partial class PuTTYControl : UserControlBase, IDragablzTabItem, IEmbedded
1717
{
1818
#region Events
1919

20-
private bool _isDpiChanging;
21-
2220
private void WindowGrid_SizeChanged(object sender, SizeChangedEventArgs e)
2321
{
2422
ResizeEmbeddedWindow();
2523
}
2624

2725
private async void WindowsFormsHost_DpiChanged(object sender, DpiChangedEventArgs e)
2826
{
29-
if (!IsConnected || _appWin == IntPtr.Zero || _isDpiChanging)
30-
return;
31-
32-
await TriggerDpiUpdateAsync();
33-
}
27+
ResizeEmbeddedWindow();
3428

35-
/// <summary>
36-
/// Detaches PuTTY's window, places it visibly on the current monitor so Windows
37-
/// delivers WM_DPICHANGED natively to PuTTY's message loop, then re-embeds it.
38-
/// Called both on initial connect (if DPIs differ) and on every DPI change.
39-
/// </summary>
40-
private async Task TriggerDpiUpdateAsync()
41-
{
42-
if (_isDpiChanging || !IsConnected || _appWin == IntPtr.Zero)
29+
if (!IsConnected || _process == null || _process.HasExited)
4330
return;
4431

45-
_isDpiChanging = true;
46-
47-
try
48-
{
49-
// Use MonitorFromWindow for reliable physical-pixel coordinates — WinForms's
50-
// Screen.FromHandle can return DPI-virtualized (wrong) coordinates on
51-
// PerMonitorV2 setups.
52-
var bounds = NativeMethods.GetMonitorBoundsForWindow(WindowHost.Handle);
53-
int cx = bounds.left + (bounds.right - bounds.left) / 2;
54-
int cy = bounds.top + (bounds.bottom - bounds.top) / 2;
55-
56-
// Temporarily restore WS_POPUP so that after SetParent(null) Windows treats
57-
// the window as a proper top-level popup. Without WS_POPUP (stripped during
58-
// embedding), the window has no recognised top-level style and Windows
59-
// skips sending WM_DPICHANGED to it entirely.
60-
// Also clear WS_CHILD if SetParent happened to set it.
61-
long style = NativeMethods.GetWindowLong(_appWin, NativeMethods.GWL_STYLE);
62-
style &= ~NativeMethods.WS_CHILD;
63-
style |= NativeMethods.WS_POPUP;
64-
NativeMethods.SetWindowLongPtr(_appWin, NativeMethods.GWL_STYLE, new IntPtr(style));
65-
66-
NativeMethods.SetParent(_appWin, IntPtr.Zero);
67-
68-
// Place as an 800×600 window at the centre of the target monitor, behind
69-
// all other windows (HWND_BOTTOM). SWP_SHOWWINDOW makes it visible so
70-
// Windows detects the monitor's DPI and delivers WM_DPICHANGED natively.
71-
// A 1×1 window is below the threshold Windows uses for DPI detection.
72-
NativeMethods.SetWindowPos(_appWin, NativeMethods.HWND_BOTTOM, cx, cy, 800, 600,
73-
NativeMethods.SWP_NOACTIVATE | NativeMethods.SWP_SHOWWINDOW);
74-
75-
// Give PuTTY's message loop time to dequeue and handle WM_DPICHANGED.
76-
await Task.Delay(300);
77-
78-
if (!IsConnected || _appWin == IntPtr.Zero)
79-
return;
80-
81-
NativeMethods.ShowWindow(_appWin, NativeMethods.WindowShowStyle.Hide);
82-
NativeMethods.SetParent(_appWin, WindowHost.Handle);
83-
84-
// Remove WS_POPUP after re-embedding; keeping it on a child window can
85-
// cause painting to overflow the parent panel (see issue #167).
86-
style = NativeMethods.GetWindowLong(_appWin, NativeMethods.GWL_STYLE);
87-
style &= ~NativeMethods.WS_POPUP;
88-
NativeMethods.SetWindowLongPtr(_appWin, NativeMethods.GWL_STYLE, new IntPtr(style));
89-
90-
ResizeEmbeddedWindow();
91-
NativeMethods.ShowWindow(_appWin, NativeMethods.WindowShowStyle.ShowNoActivate);
92-
}
93-
finally
94-
{
95-
_isDpiChanging = false;
96-
}
32+
// PuTTY is a GUI application (not console-based), so the Console Font API
33+
// (AttachConsole/SetCurrentConsoleFontEx) does not apply. Instead, send
34+
// WM_DPICHANGED directly to the PuTTY window so it can rescale its fonts
35+
// and layout internally — bypassing the cross-process delivery limitation.
36+
NativeMethods.TrySendDpiChangedMessage(
37+
_appWin,
38+
e.OldDpi.PixelsPerInchX,
39+
e.NewDpi.PixelsPerInchX);
9740
}
98-
9941
#endregion
10042

10143
#region Variables
@@ -252,8 +194,9 @@ private async Task Connect()
252194

253195
if (!_process.HasExited)
254196
{
255-
// Capture PuTTY's DPI before embedding. If it started on a
256-
// different monitor than ours, we correct below via detach/reattach.
197+
// Capture PuTTY's window DPI before embedding. The process
198+
// might have started on a different monitor than ours, so its font
199+
// may be scaled for a different DPI. We correct this after embedding.
257200
var initialWindowDpi = NativeMethods.GetDpiForWindow(_appWin);
258201

259202
// Enable mixed-DPI hosting on this thread before SetParent so that
@@ -267,11 +210,9 @@ private async Task Connect()
267210
// Show window before set style and resize
268211
NativeMethods.ShowWindow(_appWin, NativeMethods.WindowShowStyle.Maximize);
269212

270-
// Remove border etc.
213+
// Remove border etc.
271214
long style = (int)NativeMethods.GetWindowLong(_appWin, NativeMethods.GWL_STYLE);
272-
style &= ~(NativeMethods.WS_CAPTION | NativeMethods.WS_POPUP |
273-
NativeMethods
274-
.WS_THICKFRAME); // NativeMethods.WS_POPUP --> Overflow? (https://github.com/BornToBeRoot/NETworkManager/issues/167)
215+
style &= ~(NativeMethods.WS_CAPTION | NativeMethods.WS_POPUP | NativeMethods.WS_THICKFRAME);
275216
NativeMethods.SetWindowLongPtr(_appWin, NativeMethods.GWL_STYLE, new IntPtr(style));
276217

277218
IsConnected = true;
@@ -282,11 +223,13 @@ private async Task Connect()
282223

283224
ResizeEmbeddedWindow();
284225

285-
// If PuTTY started on a different DPI monitor than ours, trigger
286-
// a DPI correction now so its font matches the current monitor.
226+
// If PuTTY started at a different DPI than our panel (e.g. it
227+
// spawned on a secondary monitor with a different scale factor),
228+
// send WM_DPICHANGED so PuTTY rescales its fonts to match.
287229
var currentPanelDpi = NativeMethods.GetDpiForWindow(WindowHost.Handle);
288230
if (initialWindowDpi != currentPanelDpi)
289-
await TriggerDpiUpdateAsync();
231+
NativeMethods.TrySendDpiChangedMessage(_appWin,
232+
initialWindowDpi, currentPanelDpi);
290233
}
291234
}
292235
}

0 commit comments

Comments
 (0)