最近总是收到一个异常 “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 && 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 来决定是否抛出异常,在需要抛出异常的情况下会真实求逆,也就是上面截图中我们看到的异常发生处的代码。

那么接下来我们需要验证三点:

  1. 这个 Visual 是哪里来的;
  2. 这个 Visual 的变换矩阵什么情况下不可求逆;
  3. throwOnError 确定传入的是 true 吗。

于是我们继续往上层调用代码中查看。

可以很快验证上面需要验证的两个点:

  1. throwOnError 传入的是 true
  2. VisualPresentationSourceRootVisual

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 变换矩阵:

  1. 平移和旋转不可能导致矩阵不可逆;
  2. 缩放,只要水平和垂直方向的任何一个分量缩放量为 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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/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>

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

原因和解决方案

原因

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

解决方案

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


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

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

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

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

  1. Android窗口管理服务WindowManagerService对壁纸窗口(Wallpaper Window)的管理分析

    在Android系统中,壁纸窗口和输入法窗口一样,都是一种特殊类型的窗口,而且它们都是喜欢和一个普通的Activity窗口缠绵在一起.大家可以充分地想象这样的一个3W场景:输入法窗口在上面,壁纸窗口在 ...

  2. WPF的Style中的设置器Setter

    一.概述 Setters 是Style类中的一个重要属性. Setters 包含着 Setter 和 EventSetter 类的集合 Setter类的作用是给System.Windows.Style ...

  3. Netty消息接收类故障案例分析

    <Netty 进阶之路>.<分布式服务框架原理与实践>作者李林锋深入剖析Netty消息接收类故障案例.李林锋此后还将在 InfoQ 上开设 Netty 专题持续出稿,感兴趣的同 ...

  4. 解决WPF中重载Window.OnRender函数失效问题

    原文:解决WPF中重载Window.OnRender函数失效问题 今天实验一个绘图算法的时候,偶然发现重载Window.OnRender的方法是没有效果的. public partial class ...

  5. 转]Window, Linux动态链接库的分析对比

    转]Window, Linux动态链接库的分析对比 摘 要:动态链接库技术实现和设计程序常用的技术,在Windows和Linux系统中都有动态库的概念,采用动态库可以有效的减少程序大小,节省空间,提高 ...

  6. Android的Window类详解

    Android的Window类(一) Android的GUI层并不复杂.它的复杂度类似于WGUI这类基于布局和对话框的GUI,与MFC.Qt等大型框架没有可比性,甚至飞漫魏永明的MiniGUI都比它复 ...

  7. Window SendMessage,PostMessage分析

    Window SendMessage,PostMessage分析 背景 SendMessage 函数原型 PostMessage 函数原型 区别 问题解决 背景 前段时间程序中突然出现一个Bug,程序 ...

  8. 江苏事业单位招聘考计算机专业课,江苏事业单位专技岗-计算机类考情分析

    原标题:江苏事业单位专技岗-计算机类考情分析 一.岗位说明 江苏事业单位统考每年都会进行组织,考试类别分为:管理类岗位.通用类专业技术岗位.工勤技能类岗位.主要区别如下: (1)管理类岗位,包括事业单 ...

  9. WPF 后台常用属性值设置

    1.字体加粗 FontWeight = FontWeights.Bold; 2.设置16进制颜色值 Label.Background = new SolidColorBrush(Colors.Cade ...

最新文章

  1. 大数据处理的关键架构
  2. python3报错:importError: dynamic module does not define module export function (PyInit_cv_bridge_boost
  3. Hadoop2.6.0的FileInputFormat的任务切分原理分析(即如何控制FileInputFormat的map任务数量)...
  4. 【JXOI2018】守卫
  5. linux下系统安全常见问题2
  6. SageMaker使用托管容器训练本地网络模型
  7. 语言认知偏差_我们的认知偏差正在破坏患者的结果数据
  8. 查看nginx php mysql apache编译安装参数
  9. HTML5 Web Worker的使用
  10. oracle model类型,Oracle SQL高级编程——Model子句全解析-Oracle
  11. Chrome插件英雄榜(第二期)
  12. 三星s7edge计算机软件,三星s7edge 官方6.0固件
  13. linux可以用tab键,linux下tab键在命令行情况下的强大
  14. 让 Linux 更安全
  15. java 请假系统_JAVA 师生请假系统 课程设计
  16. AUI框架的介绍和使用
  17. 关于vue移动端下载图片
  18. matlab制作数字华容道,从技术角度实现实现数字华容道
  19. python设计模式(一)创建型模式
  20. 2016/10/20

热门文章

  1. 有限体积法(2)——二维、三维扩散方程的离散推导
  2. Go + C 一款简单的贪吃蛇
  3. 落魄前端,整理给自己的前端知识体系复习大纲(上篇,2w字)
  4. VS2015 解决 “有太多的错误导致IntelliSense引擎无法正常工作,其中有些错误无法在编辑其中查看”问题
  5. 如何将图片转换成JPG图片格式?如何将照片转换为jpg?
  6. 【JavaSE】Lambda表达式、接口组成更新、方法引用
  7. 2023前端面试题集(持续更新中~),祝大家早日拿到心仪offer
  8. Aseprite学习/技巧
  9. sklearn分类算法-决策树、随机森林
  10. GEE学习笔记(基础篇)更新中