100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > WPF 不要给 Window 类设置变换矩阵(分析篇):System.InvalidOperationException: 转换不可逆。

WPF 不要给 Window 类设置变换矩阵(分析篇):System.InvalidOperationException: 转换不可逆。

时间:2021-03-30 13:02:10

相关推荐

WPF 不要给 Window 类设置变换矩阵(分析篇):System.InvalidOperationException: 转换不可逆。

最近总是收到一个异常 “System.InvalidOperationException: 转换不可逆。”,然而看其堆栈,一点点自己写的代码都没有。到底哪里除了问题呢?

虽然异常堆栈信息里面没有自己编写的代码,但是我们还是找到了问题的原因和解决方法。

本文内容

异常堆栈分析过程源代码`PointUtil.TryApplyVisualTransform``PointUtil.TryClientToRoot` 求逆的矩阵矩阵求逆异常代码行列式缩放矩阵旋转矩阵WPF 2D 变换矩阵求逆小结 寻找问题代码 原因和解决方案原因解决方案

异常堆栈

这就是抓到的此问题的异常堆栈:

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 && positionTarget != null && !positionTarget.IsDisposed)){point = positionTarget.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传入的是trueVisualPresentationSourceRootVisual

PresentationSourceRootVisual是什么呢?PresentationSource是承载 WPF 可视化树的一个对象,对于窗口Window,是通过HwndSourcePresentationSource的子类)承载的;对于跨线程 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} ⎣⎡​M11M21OffsetX​M12M22OffsetY​001​⎦⎤​

其行列式 d e t ( M ) det(M) det(M) 是一个标量:

∣ A ∣ = ∣ 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 ∣ = M 11 × M 22 − M 12 × M 21 \left | A \right | = \begin{vmatrix} M11 & M12 & 0 \\ M21 & M22 & 0 \\ OffsetX & OffsetY & 1 \end{vmatrix} = M11 \times M22 - M12 \times M21 ∣A∣=∣∣∣∣∣∣​M11M21OffsetX​M12M22OffsetY​001​∣∣∣∣∣∣​=M11×M22−M12×M21

因为矩阵求逆的时候,行列式的值会作为分母,于是会无法计算,所以行列式的值为 0 时,矩阵不可逆。

前面我们计算 WPF 的 2D 变换矩阵的行列式的值为 M 11 × M 22 − M 12 × M 21 M11 \times M22 - M12 \times M21 M11×M22−M12×M21,因此,只要使这个式子的值为 0 即可。

那么 WPF 的 2D 变换的时候,如何使此值为 0 呢?

平移?平移只会修改 O f f s e t X OffsetX OffsetX 和 O f f s e t Y OffsetY OffsetY,因此对结果没有影响缩放?缩放会将原矩阵点乘缩放矩阵旋转?旋转会将旋转矩阵点乘原矩阵

其中,原矩阵在我们的场景下就是恒等的矩阵,即Matrix.Identity

[ 1 0 0 0 1 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} ⎣⎡​100​010​001​⎦⎤​

接下来缩放和旋转我们都不考虑变换中心的问题,因为变换中心的问题都可以等价为先进行缩放和旋转后,再单纯进行平移。由于平移对行列式的值没有影响,于是我们忽略。

缩放矩阵

缩放矩阵。如果水平和垂直分量分别缩放 S c a l e X ScaleX ScaleX 和 S c a l e Y ScaleY ScaleY 倍,则缩放矩阵为:

[ S c a l e X 0 0 0 S c a l e Y 0 0 0 1 ] \begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix} ⎣⎡​ScaleX00​0ScaleY0​001​⎦⎤​

原矩阵点乘缩放矩阵结果为:

[ 1 0 0 0 1 0 0 0 1 ] ⋅ [ S c a l e X 0 0 0 S c a l e Y 0 0 0 1 ] = [ S c a l e X 0 0 0 S c a l e Y 0 0 0 1 ] \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} ScaleX & 0 & 0 \\ 0 & ScaleY & 0 \\ 0 & 0 & 1 \end{bmatrix} ⎣⎡​100​010​001​⎦⎤​⋅⎣⎡​ScaleX00​0ScaleY0​001​⎦⎤​=⎣⎡​ScaleX00​0ScaleY0​001​⎦⎤​

于是,只要 S c a l e X ScaleX ScaleX 和 S c a l e Y ScaleY ScaleY 任何一个为 0 就可以导致新矩阵的行列式必定为 0。

旋转矩阵

旋转矩阵。假设用户设置的旋转角度为angle,那么换算成弧度为angle * (Math.PI/180.0),我们将弧度记为 α \alpha α,那么旋转矩阵为:

[ cos ⁡ α sin ⁡ α 0 − sin ⁡ α cos ⁡ α 0 0 0 1 ] \begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix} ⎣⎡​cosα−sinα0​sinαcosα0​001​⎦⎤​

旋转矩阵点乘原矩阵的结果为:

[ cos ⁡ α sin ⁡ α 0 − sin ⁡ α cos ⁡ α 0 0 0 1 ] ⋅ [ 1 0 0 0 1 0 0 0 1 ] = [ cos ⁡ α sin ⁡ α 0 − sin ⁡ α cos ⁡ α 0 0 0 1 ] \begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} \cos{\alpha} & \sin{\alpha} & 0 \\ -\sin{\alpha} & \cos{\alpha} & 0 \\ 0 & 0 & 1 \end{bmatrix} ⎣⎡​cosα−sinα0​sinαcosα0​001​⎦⎤​⋅⎣⎡​100​010​001​⎦⎤​=⎣⎡​cosα−sinα0​sinαcosα0​001​⎦⎤​

对此矩阵的行列式求值:

cos ⁡ 2 α + sin ⁡ 2 α = 1 \cos^{2}{\alpha} + \sin^{2}{\alpha} = 1 cos2α+sin2α=1

也就是说其行列式的值恒等于 1,因此其矩阵必然可求逆。

WPF 2D 变换矩阵求逆小结

对于 WPF 的 2D 变换矩阵:

平移和旋转不可能导致矩阵不可逆;缩放,只要水平和垂直方向的任何一个分量缩放量为 0,矩阵就会不可逆。

寻找问题代码

现在,我们寻找问题的方向已经非常明确了:

找到设置了ScaleTransformWindow,检查其是否给ScaleX或者ScaleY属性赋值为了0

然而,真正写一个 demo 程序来验证这个问题的时候,就发现没有这么简单。因为:

我们发现,不止是ScaleXScaleY属性不能设为0,实际上设成0.5或者其他值也是不行的。

唯一合理值是1

那么为什么依然有异常呢?难道是ScaleTransform的值一开始正常,然后被修改?

编写 demo 验证,果然如此。而只有变换到0才会真的引发本文一开始我们提到的异常。一般会开始设为1而后设为0的代码通常是在做动画。

一定是有代码正在为窗口的ScaleTransform做动画。

结果全代码仓库搜索ScaleTransform真的找到了问题代码。

<Window x:Name="WalterlvDoubiWindow"x:Class="Walterlv.Exceptions.Unknown.MainWindow"xmlns="/winfx//xaml/presentation"xmlns:x="/winfx//xaml"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Window.RenderTransform><ScaleTransform ScaleX="1" ScaleY="1" /></Window.RenderTransform><Window.Resources><Storyboard x:Key="Storyboard.Load"><DoubleAnimation Storyboard.TargetName="WalterlvDoubiWindow"Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"From="0" To="1" /><DoubleAnimation Storyboard.TargetName="WalterlvDoubiWindow"Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"From="0" To="1" /></Storyboard></Window.Resources><Grid><!-- 省略的代码 --></Grid></Window>

不过,这段代码并不会导致每次都出现异常,而是在非常多次尝试中偶尔能出现一次异常。

原因和解决方案

原因

Window类是不可以设置RenderTransform属性的,但允许设置恒等(Matrix.Identity)的变换;如果让Window类缩放分量设置为0,就会出现矩阵不可逆异常。

解决方案

不要给Window类设置变换,如果要做,请给Window内部的子元素设置。比如上面的例子中,我们给Grid设置就没有问题(而且可以做到类似的效果。

我的博客会首发于 /,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv./),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。