-
-
Notifications
You must be signed in to change notification settings - Fork 972
fix(controls): Fix flickering issue when hovering over the top-right close button #1653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
346f1cd
a2b21fa
d7a88e9
9cd3569
a2424fc
c7eaae4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -674,6 +674,134 @@ or PInvoke.WM_NCLBUTTONUP | |
| return IntPtr.Zero; | ||
| } | ||
|
|
||
| bool isMouseOverHeaderContent = false; | ||
| bool isMouseOverButtons = false; | ||
| IntPtr htResult = (IntPtr)PInvoke.HTNOWHERE; | ||
|
|
||
| // For WM_NCHITTEST, perform resize detection first, and skip button hit testing if top-left or top-right corner resize detection succeeds | ||
| if (message == PInvoke.WM_NCHITTEST) | ||
| { | ||
| if (TrailingContent is UIElement || Header is UIElement || CenterContent is UIElement) | ||
| { | ||
| UIElement? headerLeftUIElement = Header as UIElement; | ||
| UIElement? headerCenterUIElement = CenterContent as UIElement; | ||
| UIElement? headerRightUiElement = TrailingContent as UIElement; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Right" should now be "Trailing"
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, thanks. I renamed headerRightUiElement to headerTrailingUiElement to align terminology with TrailingContent. |
||
|
|
||
| isMouseOverHeaderContent = | ||
| (headerLeftUIElement is not null | ||
| && headerLeftUIElement != _titleBlock | ||
| && TitleBarButton.IsMouseOverNonClient(headerLeftUIElement, lParam)) || (headerCenterUIElement is not null | ||
| && TitleBarButton.IsMouseOverNonClient(headerCenterUIElement, lParam)) || (headerRightUiElement is not null | ||
| && TitleBarButton.IsMouseOverNonClient(headerRightUiElement, lParam)); | ||
| } | ||
|
|
||
| TitleBarButton? rightmostButton = null; | ||
| double rightmostRightEdge = double.MinValue; | ||
|
|
||
| foreach (TitleBarButton button in _buttons) | ||
| { | ||
| if (button is null) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| if (PresentationSource.FromVisual(button) is not null) | ||
| { | ||
| Point buttonTopLeft = button.PointToScreen(new Point(0, 0)); | ||
| double buttonRightEdge = buttonTopLeft.X + button.RenderSize.Width; | ||
|
|
||
| if (buttonRightEdge > rightmostRightEdge) | ||
| { | ||
| rightmostRightEdge = buttonRightEdge; | ||
| rightmostButton = button; | ||
| } | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| // Ignore visual transform errors and keep searching. | ||
| } | ||
|
|
||
| if (TitleBarButton.IsMouseOverNonClient(button, lParam)) | ||
| { | ||
| isMouseOverButtons = true; | ||
| } | ||
| } | ||
|
|
||
| htResult = GetWindowBorderHitTestResult(hwnd, lParam); | ||
|
|
||
| // If resize hit test succeeds, let Windows handle it | ||
| if (htResult != (IntPtr)PInvoke.HTNOWHERE) | ||
| { | ||
| handled = true; | ||
| return htResult; | ||
| } | ||
|
|
||
| if (rightmostButton is not null | ||
| && Windows.Win32.PInvoke.GetCursorPos(out Windows.Win32.POINT cursorPoint)) | ||
| { | ||
| Point cursorPosition = new(cursorPoint.X, cursorPoint.Y); | ||
|
|
||
| try | ||
| { | ||
| Point rightmostTopLeft = rightmostButton.PointToScreen(new Point(0, 0)); | ||
| double rightEdge = rightmostTopLeft.X + rightmostButton.RenderSize.Width; | ||
| double leftEdge = rightEdge - 1; | ||
| double bottomEdge = rightmostTopLeft.Y + rightmostButton.RenderSize.Height; | ||
|
|
||
| if ( | ||
| cursorPosition.X >= leftEdge | ||
| && cursorPosition.X <= rightEdge | ||
| && cursorPosition.Y >= rightmostTopLeft.Y | ||
| && cursorPosition.Y <= bottomEdge | ||
| ) | ||
| { | ||
| handled = true; | ||
| return (IntPtr)PInvoke.HTRIGHT; | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| // Ignore transform errors and fall back to default hit testing. | ||
| } | ||
| } | ||
|
|
||
| if (isMouseOverButtons) | ||
| { | ||
| htResult = (IntPtr)PInvoke.HTNOWHERE; | ||
| } | ||
| } | ||
| else if (message == PInvoke.WM_NCLBUTTONDOWN) | ||
| { | ||
| // For WM_NCLBUTTONDOWN, also skip button hit testing if within top-left or top-right corner resize area | ||
| // This ensures resize handling works correctly | ||
| foreach (TitleBarButton button in _buttons) | ||
| { | ||
| if (button is null) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (TitleBarButton.IsMouseOverNonClient(button, lParam)) | ||
| { | ||
| isMouseOverButtons = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| htResult = GetWindowBorderHitTestResult(hwnd, lParam); | ||
|
|
||
| if (htResult != (IntPtr)PInvoke.HTNOWHERE) | ||
| { | ||
| // If within resize area, skip button hit testing | ||
| // and let Windows handle the default resize processing | ||
| handled = false; | ||
| return IntPtr.Zero; | ||
| } | ||
| } | ||
|
|
||
| foreach (TitleBarButton button in _buttons) | ||
| { | ||
| if (!button.ReactToHwndHook(message, lParam, out IntPtr returnIntPtr)) | ||
|
|
@@ -699,25 +827,6 @@ or PInvoke.WM_NCLBUTTONUP | |
| return returnIntPtr; | ||
| } | ||
|
|
||
| bool isMouseOverHeaderContent = false; | ||
| IntPtr htResult = (IntPtr)PInvoke.HTNOWHERE; | ||
|
|
||
| if (message == PInvoke.WM_NCHITTEST) | ||
| { | ||
| if (TrailingContent is UIElement || Header is UIElement || CenterContent is UIElement) | ||
| { | ||
| UIElement? headerLeftUIElement = Header as UIElement; | ||
| UIElement? headerCenterUIElement = CenterContent as UIElement; | ||
| UIElement? headerRightUiElement = TrailingContent as UIElement; | ||
|
|
||
| isMouseOverHeaderContent = (headerLeftUIElement is not null && headerLeftUIElement != _titleBlock && headerLeftUIElement.IsMouseOverElement(lParam)) | ||
| || (headerCenterUIElement?.IsMouseOverElement(lParam) ?? false) | ||
| || (headerRightUiElement?.IsMouseOverElement(lParam) ?? false); | ||
| } | ||
|
|
||
| htResult = GetWindowBorderHitTestResult(hwnd, lParam); | ||
| } | ||
|
|
||
| var e = new HwndProcEventArgs(hwnd, msg, wParam, lParam, isMouseOverHeaderContent); | ||
| WndProcInvoked?.Invoke(this, e); | ||
|
|
||
|
|
@@ -729,14 +838,14 @@ or PInvoke.WM_NCLBUTTONUP | |
|
|
||
| switch (message) | ||
| { | ||
| case PInvoke.WM_NCHITTEST when CloseWindowByDoubleClickOnIcon && _icon.IsMouseOverElement(lParam): | ||
| case PInvoke.WM_NCHITTEST when CloseWindowByDoubleClickOnIcon && TitleBarButton.IsMouseOverNonClient(_icon, lParam): | ||
| // Ideally, clicking on the icon should open the system menu, but when the system menu is opened manually, double-clicking on the icon does not close the window | ||
| handled = true; | ||
| return (IntPtr)PInvoke.HTSYSMENU; | ||
| case PInvoke.WM_NCHITTEST when htResult != (IntPtr)PInvoke.HTNOWHERE: | ||
| handled = true; | ||
| return htResult; | ||
| case PInvoke.WM_NCHITTEST when this.IsMouseOverElement(lParam) && !isMouseOverHeaderContent: | ||
| case PInvoke.WM_NCHITTEST when TitleBarButton.IsMouseOverNonClient(this, lParam) && !isMouseOverHeaderContent: | ||
| handled = true; | ||
| return (IntPtr)PInvoke.HTCAPTION; | ||
| default: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,16 +4,28 @@ | |
| // All Rights Reserved. | ||
|
|
||
| using System.Runtime.InteropServices; | ||
| using Windows.Win32.Foundation; | ||
| using Windows.Win32.UI.WindowsAndMessaging; | ||
|
|
||
| namespace Windows.Win32; | ||
|
|
||
| internal static partial class PInvoke | ||
|
Check warning on line 11 in src/Wpf.Ui/Interop/PInvoke.cs
|
||
| { | ||
| [ | ||
| DllImport("USER32.dll", ExactSpelling = true, EntryPoint = "SetWindowLongPtrW", SetLastError = true), | ||
| DefaultDllImportSearchPaths(DllImportSearchPath.System32) | ||
| ] | ||
| internal static extern nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong); | ||
| internal static extern nint SetWindowLongPtr(Windows.Win32.Foundation.HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong); | ||
|
|
||
| [ | ||
| DllImport("USER32.dll", ExactSpelling = true, EntryPoint = "GetCursorPos", SetLastError = true), | ||
| DefaultDllImportSearchPaths(DllImportSearchPath.System32) | ||
| ] | ||
| internal static extern bool GetCursorPos(out POINT lpPoint); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use the one generated from cswin32?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the suggestion. I replaced the manual interop with the CsWin32-generated GetCursorPos API by adding it to NativeMethods.txt, removed the custom GetCursorPos/POINT declarations from Interop/PInvoke.cs, and updated call sites accordingly. |
||
| } | ||
|
|
||
| [StructLayout(LayoutKind.Sequential)] | ||
| internal struct POINT | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use the one generated from cswin32? |
||
| { | ||
| public int X; | ||
| public int Y; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of magic numbers here. Does CsWin32 generate a flags enum, perhaps? Or can we at least use constants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, thank you. I removed the magic bitmask values and introduced a named [Flags] enum (BorderHitEdges), then updated the hit-test logic to use named combinations for better readability and maintainability.