Skip to content

Commit 8389c17

Browse files
CopilotBornToBeRoot
andcommitted
Update blog post: add C#/WPF mention, XAML snippet, inline comments, remove "What Does Not Work" table, rewrite summary
Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
1 parent 440e7e8 commit 8389c17

File tree

1 file changed

+18
-18
lines changed
  • Website/blog/2026-03-04-high-dpi-embedded-processes

1 file changed

+18
-18
lines changed

Website/blog/2026-03-04-high-dpi-embedded-processes/index.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,17 @@ This article documents the investigation and the two different solutions NETwork
1313

1414
## The Embedding Technique
1515

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:
19+
20+
```xaml
21+
<WindowsFormsHost DpiChanged="WindowsFormsHost_DpiChanged">
22+
<windowsForms:Panel x:Name="WindowHost" />
23+
</WindowsFormsHost>
24+
```
25+
26+
The C# code-behind then calls `SetParent` to embed the external process window:
1727

1828
```csharp
1929
// 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
3343

3444
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.
3545

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-
4646
## Solution A — Console Host Processes (PowerShell, cmd)
4747

4848
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.
@@ -127,6 +127,9 @@ private void WindowsFormsHost_DpiChanged(object sender, DpiChangedEventArgs e)
127127
if (!IsConnected)
128128
return;
129129

130+
// Rescale the console font using the new/old DPI ratio via the Console API.
131+
// WM_DPICHANGED is never forwarded to cross-process child windows,
132+
// so we use AttachConsole + SetCurrentConsoleFontEx instead.
130133
NativeMethods.TryRescaleConsoleFont(
131134
(uint)_process.Id,
132135
e.NewDpi.PixelsPerInchX / e.OldDpi.PixelsPerInchX);
@@ -217,6 +220,9 @@ private void WindowsFormsHost_DpiChanged(object sender, DpiChangedEventArgs e)
217220
if (!IsConnected)
218221
return;
219222

223+
// 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.
220226
NativeMethods.TrySendDpiChangedMessage(
221227
_appWin,
222228
e.OldDpi.PixelsPerInchX,
@@ -267,16 +273,10 @@ The `-20` offset compensates for a layout quirk introduced by the Dragablz tab c
267273

268274
## Summary
269275

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`.
275277

276278
The full implementation is available in the NETworkManager source:
277279

278280
- [`NETworkManager.Utilities/NativeMethods.cs`](https://github.com/BornToBeRoot/NETworkManager/blob/main/Source/NETworkManager.Utilities/NativeMethods.cs) — all P/Invoke declarations and helpers
279281
- [`NETworkManager/Controls/PowerShellControl.xaml.cs`](https://github.com/BornToBeRoot/NETworkManager/blob/main/Source/NETworkManager/Controls/PowerShellControl.xaml.cs) — console host approach
280282
- [`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

Comments
 (0)