diff --git a/src/ModernOverlay.Win32/Win32WindowQuery.cs b/src/ModernOverlay.Win32/Win32WindowQuery.cs index 6ab80a1..5fa33a2 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,54 @@ 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; + } + + 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..55b344f 100644 --- a/tests/ModernOverlay.Tests/TargetTrackingTests.cs +++ b/tests/ModernOverlay.Tests/TargetTrackingTests.cs @@ -195,6 +195,48 @@ public async Task OverlayCanDiscoverTargetByProcessId() Assert.IsFalse(bounds.IsEmpty); } + [TestMethod] + [TestCategory("WindowsIntegration")] + 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"); + 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 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()