You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: Website/blog/2026-03-04-high-dpi-embedded-processes/index.md
+18-18Lines changed: 18 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,17 @@ This article documents the investigation and the two different solutions NETwork
13
13
14
14
## The Embedding Technique
15
15
16
-
NETworkManager uses `WindowsFormsHost` to host a native Win32 `Panel` (WinForms `Panel`), and then calls `SetParent` to re-parent a foreign process window into that panel:
16
+
NETworkManager is a C#/WPF application that uses `WindowsFormsHost` to host a native Win32 `Panel` (WinForms `Panel`), and then calls `SetParent` to re-parent a foreign process window into that panel.
17
+
18
+
The XAML wires up the `DpiChanged` event and embeds a WinForms `Panel` as the hosting surface:
The C# code-behind then calls `SetParent` to embed the external process window:
17
27
18
28
```csharp
19
29
// Make the external process window a child of our WinForms panel
@@ -33,16 +43,6 @@ WPF applications declare `PerMonitorV2` DPI awareness in their manifest. When th
33
43
34
44
The problem is that `_appWin` is owned by a **completely separate process** (PuTTY, conhost). From the Windows DWM compositor's perspective it is now a child window of your panel, but the DPI notification system only walks intra-process window trees. The external child window never receives any DPI message.
35
45
36
-
### What Does Not Work
37
-
38
-
Before arriving at the solutions below, several approaches were tried:
39
-
40
-
| Attempt | Why it failed |
41
-
|---------|---------------|
42
-
| Send `WM_DPICHANGED_AFTERPARENT` (0x02E3) | Causes the process to call `GetDpiForWindow` on itself — returns the DPI of its **current monitor** (now wrong because it is a child, not a top-level window) |
43
-
| Send `WM_DPICHANGED` (0x02E0) with explicit DPI in wParam | Works only for newer PuTTY builds (0.75+) that handle this message; breaks for older builds and doesn't help console processes at all |
44
-
| Hide → detach → move → re-embed |**Hiding** the window before detaching prevents the trigger: Windows only sends `WM_DPICHANGED` to **visible** top-level windows that cross a monitor DPI boundary |
45
-
46
46
## Solution A — Console Host Processes (PowerShell, cmd)
47
47
48
48
PowerShell runs inside **conhost.exe**, the Windows console host. Unlike a GUI process, conhost exposes its font settings through the Console API (`kernel32.dll`). This is a true cross-process interface: any process can attach to an existing console and modify its font without sending any window messages.
// Send WM_DPICHANGED explicitly to the PuTTY window with the new DPI.
224
+
// WM_DPICHANGED is never forwarded to cross-process child windows after SetParent,
225
+
// so we inject the message directly.
220
226
NativeMethods.TrySendDpiChangedMessage(
221
227
_appWin,
222
228
e.OldDpi.PixelsPerInchX,
@@ -267,16 +273,10 @@ The `-20` offset compensates for a layout quirk introduced by the Dragablz tab c
267
273
268
274
## Summary
269
275
270
-
| Process type | DPI change handler | Initial DPI correction |
271
-
|---|---|---|
272
-
|**Console host** (conhost.exe) |`AttachConsole` + `SetCurrentConsoleFontEx` with `newDpi / oldDpi` scale factor | Same — compare `GetDpiForWindow` before and after embed |
273
-
|**GUI process** (PuTTY, any Win32 app) | Send `WM_DPICHANGED` (0x02E0) with explicit new DPI | Same — send `WM_DPICHANGED` from old to new DPI |
274
-
| Both |`WindowsFormsHost` initial size set in physical pixels via `VisualTreeHelper.GetDpi`| — |
276
+
When you embed a foreign process window via `SetParent`, Windows never forwards DPI change notifications across process boundaries. For console host processes (PowerShell, cmd) use the Windows Console API (`AttachConsole` + `SetCurrentConsoleFontEx`) to rescale fonts directly; for GUI processes (PuTTY) send `WM_DPICHANGED` (0x02E0) explicitly with the new DPI packed into `wParam`. In both cases, apply an initial DPI correction after `SetParent` by comparing `GetDpiForWindow` before and after embedding, and set the `WindowsFormsHost` initial size in physical pixels using `VisualTreeHelper.GetDpi`.
275
277
276
278
The full implementation is available in the NETworkManager source:
277
279
278
280
-[`NETworkManager.Utilities/NativeMethods.cs`](https://github.com/BornToBeRoot/NETworkManager/blob/main/Source/NETworkManager.Utilities/NativeMethods.cs) — all P/Invoke declarations and helpers
-[`NETworkManager/Controls/PuTTYControl.xaml.cs`](https://github.com/BornToBeRoot/NETworkManager/blob/main/Source/NETworkManager/Controls/PuTTYControl.xaml.cs) — GUI process approach
281
-
282
-
If you encounter a similar cross-process embedding scenario, open an [issue on GitHub](https://github.com/BornToBeRoot/NETworkManager/issues) — we are happy to discuss edge cases.
0 commit comments