From 6c11211bb5b6fecb809ef3d240fa9b5c77aba255 Mon Sep 17 00:00:00 2001 From: Sam Bent Date: Mon, 2 Aug 2021 16:30:54 -0700 Subject: [PATCH] Port WindowChrome fix from 4.8 --- .../Windows/InterOp/HwndMouseInputProvider.cs | 160 +++++++++++++++++- .../Windows/Shell/WindowChromeWorker.cs | 33 ++++ 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndMouseInputProvider.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndMouseInputProvider.cs index 99dc4847592..b38ca8f9066 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndMouseInputProvider.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndMouseInputProvider.cs @@ -7,6 +7,7 @@ using System.Windows.Media; using System.Runtime.InteropServices; using System.Diagnostics; +using System.Reflection; using System.Security; using MS.Internal; using MS.Internal.Interop; @@ -908,12 +909,12 @@ private void PossiblyDeactivate(IntPtr hwndCapture, bool stillActiveIfOverSelf) //Console.WriteLine("PossiblyDeactivate(" + hwndCapture + ")"); // We are now longer active ourselves, but it is possible that the - // window the mouse is going to intereact with is in the same + // window the mouse is going to interact with is in the same // Dispatcher as ourselves. If so, we don't want to deactivate the // mouse input stream because the other window hasn't activated it // yet, and it may result in the input stream "flickering" between - // active/inactive/active. This is ugly, so we try to supress the - // uneccesary transitions. + // active/inactive/active. This is ugly, so we try to suppress the + // unnecessary transitions. // IntPtr hwndToCheck = hwndCapture; if(hwndToCheck == IntPtr.Zero) @@ -951,17 +952,16 @@ private void PossiblyDeactivate(IntPtr hwndCapture, bool stillActiveIfOverSelf) { // We need to check if the point is over the client or // non-client area. We only care about being over the - // non-client area. + // client area. try { - NativeMethods.RECT rcClient = new NativeMethods.RECT(); - SafeNativeMethods.GetClientRect(new HandleRef(this,hwndToCheck), ref rcClient); + NativeMethods.RECT rcClient = GetEffectiveClientRect(hwndToCheck); SafeNativeMethods.ScreenToClient(new HandleRef(this,hwndToCheck), ptCursor); if(ptCursor.x < rcClient.left || ptCursor.x >= rcClient.right || ptCursor.y < rcClient.top || ptCursor.y >= rcClient.bottom) { - // We are not over the non-client area. We can bail out. + // We are not over the client area. We can bail out. //Console.WriteLine(" No capture, mouse outside of client area."); //Console.WriteLine(" Client Area: ({0},{1})-({2},{3})", rcClient.left, rcClient.top, rcClient.right, rcClient.bottom); //Console.WriteLine(" Mouse: ({0},{1})", ptCursor.x, ptCursor.y); @@ -1005,6 +1005,139 @@ private void PossiblyDeactivate(IntPtr hwndCapture, bool stillActiveIfOverSelf) } } + /// + /// Returns the effective client rect of the hwnd, for use by PossiblyDeactivate + /// when deciding whether to do the "don't deactivate mouse input stream" + /// optimization. Specifically: + /// o If the hwnd isn't ours, return Empty. We never want to do the + /// optimization in this case. + /// o If the hwnd has no custom chrome, return its native client rect. + /// o Otherwise, remove the invisible caption and resize-border areas + /// from the native client rect, and return the result. + /// The last case deactivates the mouse stream when the mouse enters the + /// "effective" non-client area of a custom-chromed window. Leaving it + /// active leads to mouse events intended only for DWM to be delivered + /// to our windows, running user's event handlers at inappropriate times, + /// which can cause more or less arbitrary damage. + /// + /// For example, moving the mouse from one window into the caption area + /// of a chromed window, then press-and-drag in the caption area will + /// (correctly) cause DWM to move the window, but will also (incorrectly) + /// raise MouseMove events to both windows. [DDVSO 1245503] + /// + private NativeMethods.RECT GetEffectiveClientRect(IntPtr hwnd) + { + NativeMethods.RECT rcClient = new NativeMethods.RECT(); + HwndSource hwndSource; + + // if the hwnd isn't ours, return an empty rect + if (!IsOurWindowImpl(hwnd, out hwndSource)) + { + return rcClient; + } + + // if the hwnd has custom chrome, return the reduced client area + if (HasCustomChrome(hwndSource, ref rcClient)) + { + return rcClient; + } + + // otherwise, return the native client rect + SafeNativeMethods.GetClientRect(new HandleRef(this,hwnd), ref rcClient); + return rcClient; + } + + /// + /// If the given hwndSource has custom chrome via WindowChrome, return + /// true and set rcClient to the effective client area. + /// + private bool HasCustomChrome(HwndSource hwndSource, ref NativeMethods.RECT rcClient) + { + if (!EnsureFrameworkAccessors(hwndSource)) + { + return false; + } + + // an hwnd has custom chrome if its root visual is a Window whose + // WindowChromeWorker property is set. We can't say it that way here, + // because those classes belong to PresentationFramework. But we + // can do the equivalent using dependency properties and reflection. + DependencyObject rootVisual = hwndSource.RootVisual; + + DependencyObject windowChromeWorker = rootVisual?.GetValue(WindowChromeWorkerProperty) as DependencyObject; + if (windowChromeWorker == null) + { + return false; + } + + // ask the worker for the effective client area + object[] args = new object[1] { rcClient }; + if ((bool)GetEffectiveClientAreaMI.Invoke(windowChromeWorker, args)) + { + // copy the answer back to our caller + rcClient = (NativeMethods.RECT)(args[0]); + return true; + } + + return false; + } + + // lazy initialization of static fields that have to be + // set by reflection into PresentationFramework + private bool EnsureFrameworkAccessors(HwndSource hwndSource) + { + // if we've already done the work, return + if (WindowChromeWorkerProperty != null) + { + return true; + } + + // get a reference to PresentationFramework, either from our own + // HwndSource, or (as a fallback) from the target HwndSource + Assembly presentationFramework = GetPresentationFrameworkFromHwndSource(_source.Value); + if (presentationFramework == null) + { + presentationFramework = GetPresentationFrameworkFromHwndSource(hwndSource); + } + + if (presentationFramework == null) + { + return false; + } + + // reflect into PresentationFramework to the accessors we need + Type windowChromeWorker = presentationFramework.GetType("System.Windows.Shell.WindowChromeWorker"); + FieldInfo fiWindowChromeWorkerProperty = windowChromeWorker?.GetField("WindowChromeWorkerProperty", BindingFlags.Static | BindingFlags.Public); + DependencyProperty windowChromeWorkerProperty = fiWindowChromeWorkerProperty?.GetValue(null) as DependencyProperty; + GetEffectiveClientAreaMI = windowChromeWorker?.GetMethod("GetEffectiveClientArea", BindingFlags.Instance | BindingFlags.NonPublic); + + // if we got them all, set WindowChromeWorkerProperty to signal that the + // initialization succeeded. This method can run on multiple threads, + // but it sets the static members to the same value on each thread, so + // it doesn't need any locking. + if (windowChromeWorkerProperty != null && GetEffectiveClientAreaMI != null) + { + WindowChromeWorkerProperty = windowChromeWorkerProperty; + } + + return (WindowChromeWorkerProperty != null); + } + + // return the Assembly for PresentationFramework. Usually the root visual + // of the given HwndSource is an object whose type is (or derives from) + // a type in PF: Window, PopupRoot, etc. + private Assembly GetPresentationFrameworkFromHwndSource(HwndSource hwndSource) + { + DependencyObject rootVisual = hwndSource?.RootVisual; + + Type type = rootVisual?.GetType(); + while (type != null && type.Assembly.FullName != PresentationFrameworkAssemblyFullName) + { + type = type.BaseType; + } + + return type?.Assembly; + } private void StartTracking(IntPtr hwnd) { if(!_tracking && !_isDwmProcess) @@ -1047,14 +1180,20 @@ private IntPtr MakeLPARAM(int high, int low) } private bool IsOurWindow(IntPtr hwnd) + { + HwndSource hwndSource; + return IsOurWindowImpl(hwnd, out hwndSource); + } + + private bool IsOurWindowImpl(IntPtr hwnd, out HwndSource hwndSource) { bool isOurWindow = false; + hwndSource = null; Debug.Assert(null != _source && null != _source.Value); if(hwnd != IntPtr.Zero) { - HwndSource hwndSource; hwndSource = HwndSource.CriticalFromHwnd(hwnd); if(hwndSource != null) @@ -1341,6 +1480,11 @@ private void RecordMouseMove(int x, int y, int timestamp) private NativeMethods.TRACKMOUSEEVENT _tme = new NativeMethods.TRACKMOUSEEVENT(); + // accessors into PresentationFramework classes + const string PresentationFrameworkAssemblyFullName = "PresentationFramework, Version=" + BuildInfo.WCP_VERSION + ", Culture=neutral, PublicKeyToken=" + BuildInfo.WCP_PUBLIC_KEY_TOKEN; + private static DependencyProperty WindowChromeWorkerProperty; + private static MethodInfo GetEffectiveClientAreaMI; + // MITIGATION_SETCURSOR // // Windows can have a "context help" button in the title bar. When the user diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Shell/WindowChromeWorker.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Shell/WindowChromeWorker.cs index 1df7b8c41da..bbceb4d9af2 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Shell/WindowChromeWorker.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Shell/WindowChromeWorker.cs @@ -1313,6 +1313,39 @@ private HT _HitTestNca(Rect windowPosition, Point mousePosition) return ht; } + // Return the effective client area, excluding the invisible caption + // and resize-border areas. + // This method is called via private reflection from PresentationCore, + // method HwndMouseInputProvider.HasCustomChrome. Both places have to + // agree on the signature. + private bool GetEffectiveClientArea(ref MS.Win32.NativeMethods.RECT rcClient) + { + if (_window == null || _chromeInfo == null) + return false; + + DpiScale dpi = _window.GetDpi(); + double captionHeight = _chromeInfo.CaptionHeight; + Thickness resizeBorderThickness = _chromeInfo.ResizeBorderThickness; + + RECT rcWindow = NativeMethods.GetWindowRect(_hwnd); + Size logicalSize = DpiHelper.DeviceSizeToLogical(new Size(rcWindow.Width, rcWindow.Height), dpi.DpiScaleX, dpi.DpiScaleY); + + Point logicalTopLeft = new Point(resizeBorderThickness.Left, + resizeBorderThickness.Top + captionHeight); + Point logicalBottomRight = new Point(logicalSize.Width - resizeBorderThickness.Right, + logicalSize.Height - resizeBorderThickness.Bottom); + + Point deviceTopLeft = DpiHelper.LogicalPixelsToDevice(logicalTopLeft, dpi.DpiScaleX, dpi.DpiScaleY); + Point deviceBottomRight = DpiHelper.LogicalPixelsToDevice(logicalBottomRight, dpi.DpiScaleX, dpi.DpiScaleY); + + rcClient.left = (int)deviceTopLeft.X; + rcClient.top = (int)deviceTopLeft.Y; + rcClient.right = (int)deviceBottomRight.X; + rcClient.bottom = (int)deviceBottomRight.Y; + + return true; + } + #region Remove Custom Chrome Methods private void _RestoreStandardChromeState(bool isClosing)