diff --git a/src/ModernOverlay.Win32/NativeMethods.cs b/src/ModernOverlay.Win32/NativeMethods.cs index 08e41c5..8c5f7bd 100644 --- a/src/ModernOverlay.Win32/NativeMethods.cs +++ b/src/ModernOverlay.Win32/NativeMethods.cs @@ -72,6 +72,7 @@ internal static class NativeMethods internal const int VkRWin = 0x5C; internal static readonly nint HwndTopMost = new(-1); + internal static readonly nint HwndTop = new(0); internal static readonly nint HwndNoTopMost = new(-2); internal static readonly nint DpiAwarenessContextPerMonitorAwareV2 = new(-4); diff --git a/src/ModernOverlay.Win32/Win32WindowZOrder.cs b/src/ModernOverlay.Win32/Win32WindowZOrder.cs index e677f4d..fb61547 100644 --- a/src/ModernOverlay.Win32/Win32WindowZOrder.cs +++ b/src/ModernOverlay.Win32/Win32WindowZOrder.cs @@ -14,12 +14,23 @@ public static void RemoveTopmost(nint hwnd) public static void PlaceAbove(nint hwnd, nint hwndInsertAfter) { + if (!Win32WindowQuery.IsWindow(hwnd)) + { + throw new ArgumentException("The HWND must be a valid window.", nameof(hwnd)); + } + if (!Win32WindowQuery.IsWindow(hwndInsertAfter)) { throw new ArgumentException("The relative z-order HWND must be a valid window.", nameof(hwndInsertAfter)); } - SetZOrder(hwnd, hwndInsertAfter, "SetWindowPos(place above)"); + nint previousWindow = NativeMethods.GetWindow(hwndInsertAfter, NativeMethods.GwHwndPrev); + if (previousWindow == hwnd) + { + return; + } + + SetZOrder(hwnd, previousWindow == 0 ? NativeMethods.HwndTop : previousWindow, "SetWindowPos(place above)"); } private static void SetZOrder(nint hwnd, nint insertAfter, string operation) diff --git a/tests/ModernOverlay.Tests/Win32StyleTests.cs b/tests/ModernOverlay.Tests/Win32StyleTests.cs index 31ef57b..8c6ed24 100644 --- a/tests/ModernOverlay.Tests/Win32StyleTests.cs +++ b/tests/ModernOverlay.Tests/Win32StyleTests.cs @@ -98,6 +98,58 @@ public void Win32QueryExposesRequiredWindowMetadata() Assert.AreEqual(Win32WindowDisplayAffinity.None, Win32WindowEffects.GetDisplayAffinity(window.Hwnd)); } + [TestMethod] + [TestCategory("WindowsIntegration")] + public void PlaceAbovePositionsWindowImmediatelyAboveRelativeWindow() + { + using Win32OverlayWindow target = Win32OverlayWindow.Create(new Win32OverlayWindowOptions( + ClassName: null, + Title: "ModernOverlay z-order target", + X: 20, + Y: 20, + Width: 160, + Height: 90, + ClickThrough: false, + TopMost: false, + ToolWindow: true)); + using Win32OverlayWindow overlay = Win32OverlayWindow.Create(new Win32OverlayWindowOptions( + ClassName: null, + Title: "ModernOverlay z-order overlay", + X: 20, + Y: 20, + Width: 160, + Height: 90, + ClickThrough: true, + TopMost: false, + ToolWindow: true)); + + target.Show(); + overlay.Show(); + + Win32WindowZOrder.PlaceAbove(overlay.Hwnd, target.Hwnd); + + Assert.IsTrue(Win32WindowQuery.TryGetPreviousWindow(target.Hwnd, out nint previous)); + Assert.AreEqual(overlay.Hwnd, previous); + } + + [TestMethod] + [TestCategory("WindowsIntegration")] + public void PlaceAboveRejectsInvalidMovedWindowHandle() + { + using Win32OverlayWindow target = Win32OverlayWindow.Create(new Win32OverlayWindowOptions( + ClassName: null, + Title: "ModernOverlay invalid z-order target", + X: 20, + Y: 20, + Width: 160, + Height: 90, + ClickThrough: false, + TopMost: false, + ToolWindow: true)); + + Assert.ThrowsExactly(() => Win32WindowZOrder.PlaceAbove(0, target.Hwnd)); + } + [TestMethod] [TestCategory("WindowsIntegration")] public async Task OverlayOptionAppliesCaptureExclusionBeforeUse()