From e5125c6d45cecc0e2d33c02eec68aa5597775778 Mon Sep 17 00:00:00 2001 From: Steffen Carlsen Date: Mon, 1 Jun 2026 14:58:27 +0200 Subject: [PATCH 1/2] fix: prefer visible process target windows --- src/ModernOverlay.Win32/Win32WindowQuery.cs | 57 ++++++++++++++++++- .../TargetTrackingTests.cs | 23 +++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/ModernOverlay.Win32/Win32WindowQuery.cs b/src/ModernOverlay.Win32/Win32WindowQuery.cs index 6ab80a1..810b885 100644 --- a/src/ModernOverlay.Win32/Win32WindowQuery.cs +++ b/src/ModernOverlay.Win32/Win32WindowQuery.cs @@ -50,7 +50,7 @@ public static bool TryFindWindowByProcessName(string processName, out nint hwnd) { ArgumentException.ThrowIfNullOrWhiteSpace(processName); string normalizedProcessName = NormalizeProcessName(processName); - return TryFindTopLevelWindow(window => + return TryFindBestTopLevelWindow(window => { _ = NativeMethods.GetWindowThreadProcessId(window, out uint processId); if (processId == 0) @@ -77,7 +77,7 @@ public static bool TryFindWindowByProcessName(string processName, out nint hwnd) public static bool TryFindWindowByProcessId(int processId, out nint hwnd) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(processId); - return TryFindTopLevelWindow(window => + return TryFindBestTopLevelWindow(window => { _ = NativeMethods.GetWindowThreadProcessId(window, out uint nativeProcessId); return nativeProcessId == processId; @@ -254,6 +254,59 @@ private static bool TryFindTopLevelWindow(Func predicate, out nint h return hwnd != 0; } + private static bool TryFindBestTopLevelWindow(Func predicate, out nint hwnd) + { + nint best = 0; + int bestScore = int.MinValue; + _ = NativeMethods.EnumWindows((window, _) => + { + if (!NativeMethods.IsWindow(window) || !predicate(window)) + { + return true; + } + + int score = ScoreTopLevelWindow(window); + if (score > bestScore) + { + best = window; + bestScore = score; + } + + return true; + }, 0); + + hwnd = best; + return hwnd != 0; + } + + private static int ScoreTopLevelWindow(nint hwnd) + { + int score = 0; + if (NativeMethods.IsWindowVisible(hwnd)) + { + score += 100; + } + + nint owner = NativeMethods.GetWindow(hwnd, NativeMethods.GwOwner); + if (owner == 0 || !NativeMethods.IsWindow(owner)) + { + score += 20; + } + + nint extendedStyle = NativeMethods.GetWindowLongPtr(hwnd, WindowStyles.GwlExStyle); + if ((extendedStyle.ToInt64() & WindowStyles.WsExToolWindow) == 0) + { + score += 10; + } + + if (!string.IsNullOrWhiteSpace(GetWindowTitle(hwnd))) + { + score += 1; + } + + return score; + } + private static bool TryFindChildWindow(nint parentHwnd, Func predicate, out nint hwnd) { hwnd = 0; diff --git a/tests/ModernOverlay.Tests/TargetTrackingTests.cs b/tests/ModernOverlay.Tests/TargetTrackingTests.cs index e802125..111ca8e 100644 --- a/tests/ModernOverlay.Tests/TargetTrackingTests.cs +++ b/tests/ModernOverlay.Tests/TargetTrackingTests.cs @@ -188,13 +188,34 @@ public async Task OverlayCanDiscoverTargetByProcessId() await using OverlayWindow overlay = await OverlayWindow.CreateAsync(new OverlayWindowOptions { IsVisible = false, - Target = WindowTarget.ByProcessId(Environment.ProcessId), + Target = WindowTarget.ByProcessName(Process.GetCurrentProcess().ProcessName), }); Assert.IsTrue(Win32WindowQuery.TryGetWindowBounds(overlay.Hwnd.Value, clientArea: false, out Win32WindowBounds bounds)); Assert.IsFalse(bounds.IsEmpty); } + [TestMethod] + [TestCategory("WindowsIntegration")] + public async Task ProcessTargetDiscoveryPrefersVisibleMainWindow() + { + using Win32OverlayWindow hiddenHelper = CreateHiddenTarget(10, 20, 100, 80, title: "ModernOverlay hidden process helper"); + using Win32OverlayWindow visibleMain = CreateHiddenTarget(160, 180, 260, 140, title: "ModernOverlay visible process main"); + visibleMain.Show(); + + await using OverlayWindow overlay = await OverlayWindow.CreateAsync(new OverlayWindowOptions + { + IsVisible = false, + Target = WindowTarget.ByProcessId(Environment.ProcessId), + }); + + Assert.IsTrue(Win32WindowQuery.TryGetWindowBounds(overlay.Hwnd.Value, clientArea: false, out Win32WindowBounds bounds)); + Assert.AreEqual(160, bounds.X); + Assert.AreEqual(180, bounds.Y); + Assert.AreEqual(260, bounds.Width); + Assert.AreEqual(140, bounds.Height); + } + [TestMethod] [TestCategory("WindowsIntegration")] public async Task OverlayCanDiscoverTargetFromProvider() From 4592edd0a693a3a3246d4ed79d736f8dbdff10fe Mon Sep 17 00:00:00 2001 From: Steffen Carlsen Date: Mon, 1 Jun 2026 15:10:39 +0200 Subject: [PATCH 2/2] fix: address process window review feedback --- src/ModernOverlay.Win32/Win32WindowQuery.cs | 5 ---- .../TargetTrackingTests.cs | 25 +++++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/ModernOverlay.Win32/Win32WindowQuery.cs b/src/ModernOverlay.Win32/Win32WindowQuery.cs index 810b885..5fa33a2 100644 --- a/src/ModernOverlay.Win32/Win32WindowQuery.cs +++ b/src/ModernOverlay.Win32/Win32WindowQuery.cs @@ -299,11 +299,6 @@ private static int ScoreTopLevelWindow(nint hwnd) score += 10; } - if (!string.IsNullOrWhiteSpace(GetWindowTitle(hwnd))) - { - score += 1; - } - return score; } diff --git a/tests/ModernOverlay.Tests/TargetTrackingTests.cs b/tests/ModernOverlay.Tests/TargetTrackingTests.cs index 111ca8e..55b344f 100644 --- a/tests/ModernOverlay.Tests/TargetTrackingTests.cs +++ b/tests/ModernOverlay.Tests/TargetTrackingTests.cs @@ -188,7 +188,7 @@ public async Task OverlayCanDiscoverTargetByProcessId() await using OverlayWindow overlay = await OverlayWindow.CreateAsync(new OverlayWindowOptions { IsVisible = false, - Target = WindowTarget.ByProcessName(Process.GetCurrentProcess().ProcessName), + Target = WindowTarget.ByProcessId(Environment.ProcessId), }); Assert.IsTrue(Win32WindowQuery.TryGetWindowBounds(overlay.Hwnd.Value, clientArea: false, out Win32WindowBounds bounds)); @@ -197,7 +197,7 @@ public async Task OverlayCanDiscoverTargetByProcessId() [TestMethod] [TestCategory("WindowsIntegration")] - public async Task ProcessTargetDiscoveryPrefersVisibleMainWindow() + public async Task ProcessIdTargetDiscoveryPrefersVisibleMainWindow() { using Win32OverlayWindow hiddenHelper = CreateHiddenTarget(10, 20, 100, 80, title: "ModernOverlay hidden process helper"); using Win32OverlayWindow visibleMain = CreateHiddenTarget(160, 180, 260, 140, title: "ModernOverlay visible process main"); @@ -216,6 +216,27 @@ public async Task ProcessTargetDiscoveryPrefersVisibleMainWindow() Assert.AreEqual(140, bounds.Height); } + [TestMethod] + [TestCategory("WindowsIntegration")] + public async Task ProcessNameTargetDiscoveryPrefersVisibleMainWindow() + { + using Win32OverlayWindow hiddenHelper = CreateHiddenTarget(10, 20, 100, 80, title: "ModernOverlay hidden process helper"); + using Win32OverlayWindow visibleMain = CreateHiddenTarget(160, 180, 260, 140, title: "ModernOverlay visible process main"); + visibleMain.Show(); + + await using OverlayWindow overlay = await OverlayWindow.CreateAsync(new OverlayWindowOptions + { + IsVisible = false, + Target = WindowTarget.ByProcessName(Process.GetCurrentProcess().ProcessName), + }); + + Assert.IsTrue(Win32WindowQuery.TryGetWindowBounds(overlay.Hwnd.Value, clientArea: false, out Win32WindowBounds bounds)); + Assert.AreEqual(160, bounds.X); + Assert.AreEqual(180, bounds.Y); + Assert.AreEqual(260, bounds.Width); + Assert.AreEqual(140, bounds.Height); + } + [TestMethod] [TestCategory("WindowsIntegration")] public async Task OverlayCanDiscoverTargetFromProvider()