最近总是收到一个异常 “System.InvalidOperationException: 转换不可逆。
”,然而看其堆栈,一点点自己写的代码都没有。到底哪里除了问题呢?
虽然异常堆栈信息里面没有自己编写的代码,但是我们还是找到了问题的原因和解决方法。
本文内容
异常堆栈
这就是抓到的此问题的异常堆栈:
System.InvalidOperationException: 转换不可逆。
在 System.Windows.Media.Matrix.Invert()
在 MS.Internal.PointUtil.TryApplyVisualTransform(Point point, Visual v, Boolean inverse, Boolean throwOnError, Boolean& success)
在 MS.Internal.PointUtil.TryClientToRoot(Point point, PresentationSource presentationSource, Boolean throwOnError, Boolean& success)
在 System.Windows.Input.MouseDevice.LocalHitTest(Boolean clientUnits, Point pt, PresentationSource inputSource, IInputElement& enabledHit, IInputElement& originalHit)
在 System.Windows.Input.MouseDevice.GlobalHitTest(Boolean clientUnits, Point pt, PresentationSource inputSource, IInputElement& enabledHit, IInputElement& originalHit)
在 System.Windows.Input.StylusWisp.WispStylusDevice.FindTarget(PresentationSource inputSource, Point position)
在 System.Windows.Input.StylusWisp.WispLogic.PreNotifyInput(Object sender, NotifyInputEventArgs e)
在 System.Windows.Input.InputManager.ProcessStagingArea()
在 System.Windows.Input.InputManager.ProcessInput(InputEventArgs input)
在 System.Windows.Input.StylusWisp.WispLogic.InputManagerProcessInput(Object oInput)
在 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
在 System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
可以看到,我们的堆栈结束点是 ExceptionWrapper.TryCatchWhen
可以得知此异常是通过 Dispatcher.UnhandledException
来捕获的。也就是说,此异常直接通过 Windows 消息被我们间接触发,而不是直接通过我们编写的代码触发。而最顶端是对矩阵求逆,而此异常是试图对一个不可逆的矩阵求逆。
分析过程
如果你不想看分析过程,可以直接移步至本文的最后一节看原因和解决方案。
源代码
因为 .NET Framework 版本的 WPF 是开源的,.NET Core 版本的 WPF 目前还处于按揭开源的状态,所以我们看 .NET Framework 版本的代码来分析原因。
我按照调用堆栈从顶到底的顺序,将前面三帧的代码贴到下面。
PointUtil.TryApplyVisualTransform
public static Point TryApplyVisualTransform(Point point, Visual v, bool inverse, bool throwOnError, out bool success)
{
success = true;
if(v != null)
{
Matrix m = GetVisualTransform(v);
if (inverse)
{
if(throwOnError || m.HasInverse)
{
m.Invert();
}
else
{
success = false;
return new Point(0,0);
}
}
point = m.Transform(point);
}
return point;
}
PointUtil.TryClientToRoot
[SecurityCritical,SecurityTreatAsSafe]
public static Point TryClientToRoot(Point point, PresentationSource presentationSource, bool throwOnError, out bool success)
{
if (throwOnError || (presentationSource != null && presentationSource.CompositionTarget != null && !presentationSource.CompositionTarget.IsDisposed))
{
point = presentationSource.CompositionTarget.TransformFromDevice.Transform(point);
point = TryApplyVisualTransform(point, presentationSource.RootVisual, true, throwOnError, out success);
}
else
{
success = false;
return new Point(0,0);
}
return point;
}
你可能会说,在调用堆栈上面看不到 PointUtil.ClientToRoot
方法。但其实如果我们看一看 MouseDevice.LocalHitTest
的代码,会发现其实调用的是 PointUtil.ClientToRoot
方法。在调用堆栈上面看不到它是因为方法足够简单,被内联了。
[SecurityCritical,SecurityTreatAsSafe]
public static Point ClientToRoot(Point point, PresentationSource presentationSource)
{
bool success = true;
return TryClientToRoot(point, presentationSource, true, out success);
}
求逆的矩阵
下面我们一步一步分析异常的原因。
我们先看看是什么代码在做矩阵求逆。下面截图中的方法是反编译的,就是上面我们在源代码中列出的 TryApplyVisualTransform
方法。
先获取了传入 Visual
对象的变换矩阵,然后根据参数 inverse
来对其求逆。如果矩阵可以求逆,即 HasInverse
属性返回 true
,那么代码可以继续执行下去而不会出现异常。但如果 HasInverse
返回 false
,则根据 throwOnError
来决定是否抛出异常,在需要抛出异常的情况下会真实求逆,也就是上面截图中我们看到的异常发生处的代码。
那么接下来我们需要验证三点:
- 这个
Visual
是哪里来的; - 这个
Visual
的变换矩阵什么情况下不可求逆; throwOnError
确定传入的是true
吗。
于是我们继续往上层调用代码中查看。
可以很快验证上面需要验证的两个点:
throwOnError
传入的是true
;Visual
是PresentationSource
的RootVisual
。
而 PresentationSource
的 RootVisual
是什么呢?PresentationSource
是承载 WPF 可视化树的一个对象,对于窗口 Window
,是通过 HwndSource
(PresentationSource
的子类)承载的;对于跨线程 WPF UI,可以通过自定义的 PresentationSource
子类来完成。这部分可以参考我之前的一些博客:
- WPF 同一窗口内的多线程 UI(VisualTarget)
- WPF 同一窗口内的多线程/多进程 UI(使用 SetParent 嵌入另一个窗口)
- WPF 多线程 UI:设计一个异步加载 UI 的容器
- WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片
不管怎么说,这个指的就是 WPF 可视化树的根:
- 如果你使用
Window
来显示 WPF 窗口,那么根就是Window
类; - 如果你是用
Popup
来承载一个弹出框,那么根就是PopupRoot
类; - 如果你使用了一些跨线程/跨进程 UI 的技术,那么根就是自己写的可视化树根元素。
对于绝大多数 WPF 开发者来说,只会碰到前面第一种情况,也就是仅仅有 Window
作为可视化树的根的情况。一般人很难直接给 PopupRoot
设置变换矩阵,一般 WPF 程序的代码也很少做跨线程或跨进程 UI。
于是我们几乎可以肯定,是有某处的代码让 Window
的变换矩阵不可逆了。
矩阵求逆
什么样的矩阵是不可逆的?
异常代码
发生异常的代码是 WPF 中 Matrix.Invert
方法,其发生异常的代码如下:
首先判断矩阵的行列式 Determinant
是否为 0
,如果为 0
则抛出矩阵不可逆的异常。
行列式
WPF 的 2D 变换矩阵 M M M 是一个 3 × 3 3\times{3} 3×3 的矩阵:
[ M 11 M 12 0 M 21 M 22 0 O f f s e t X O f f s e t Y 1 ] \begin{bmatrix} M11 & M12 & 0 \\ M21 & M22 & 0 \\ OffsetX & OffsetY & 1 \end{bmatrix}