当前位置: 首页 > news >正文

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 && 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

应用变换的调用 2

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

  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 方法,其发生异常的代码如下:

Matrix.Invert

首先判断矩阵的行列式 Determinant 是否为 0,如果为 0 则抛出矩阵不可逆的异常。

Matrix.Determinant

行列式

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} M11M21OffsetXM12M22OffsetY001

其行列式 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=M11M21OffsetXM12M22OffsetY001=M11×M22M12×M21

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

前面我们计算 WPF 的 2D 变换矩阵的行列式的值为 M 11 × M 22 − M 12 × M 21 M11 \times M22 - M12 \times M21 M11×M22M12×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} 100010001

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

缩放矩阵

缩放矩阵。如果水平和垂直分量分别缩放 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} ScaleX000ScaleY0001

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

[ 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} 100010001ScaleX000ScaleY0001=ScaleX000ScaleY0001

于是,只要 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α0sinαcosα0001

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

[ 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α0sinαcosα0001100010001=cosαsinα0sinαcosα0001

对此矩阵的行列式求值:

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 程序来验证这个问题的时候,就发现没有这么简单。因为:

不能给 Window 设置变换矩阵

我们发现,不止是 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 类设置变换矩阵(应用篇)
  • git fetch 失败,因为 unable to resolve reference 'refs/remotes/origin/xxx': reference broken
  • .NET/C# 阻止屏幕关闭,阻止系统进入睡眠状态
  • 发现电脑屏幕总是不自动关闭?看看你是否打开了这些程序……
  • 临时编写和调试 C++ 代码?用 VSCode 就够了!一分钟搭好 C++ 调试环境
  • WPF 制作高性能的透明背景异形窗口(使用 WindowChrome 而不要使用 AllowsTransparency=True)
  • 在 WPF 中获取一个依赖对象的所有依赖项属性
  • 如何在 WPF 中获取所有已经显式赋过值的依赖项属性
  • .NET 设计一套高性能的弱事件机制
  • .NET/C# 利用 Walterlv.WeakEvents 高性能地中转一个自定义的弱事件(可让任意 CLR 事件成为弱事件)
  • .NET/C# 利用 Walterlv.WeakEvents 高性能地定义和使用弱事件
  • WPF 程序如何跨窗口/跨进程设置控件焦点
  • 使用 SetParent 制作父子窗口的时候,如何设置子窗口的窗口样式以避免抢走父窗口的焦点
  • WPF 程序如何移动焦点到其他控件
  • EFI 分区/恢复分区不可删除?你需要使用命令行了(配合鼠标操作)
  • 【RocksDB】TransactionDB源码分析
  • 【跃迁之路】【669天】程序员高效学习方法论探索系列(实验阶段426-2018.12.13)...
  • canvas 高仿 Apple Watch 表盘
  • iOS | NSProxy
  • JavaScript-Array类型
  • Linux CTF 逆向入门
  • log4j2输出到kafka
  • Making An Indicator With Pure CSS
  • Markdown 语法简单说明
  • maven工程打包jar以及java jar命令的classpath使用
  • nodejs实现webservice问题总结
  • Python实现BT种子转化为磁力链接【实战】
  • Service Worker
  • 计算机常识 - 收藏集 - 掘金
  • 微信小程序--------语音识别(前端自己也能玩)
  • 学习Vue.js的五个小例子
  • ​力扣解法汇总946-验证栈序列
  • #快捷键# 大学四年我常用的软件快捷键大全,教你成为电脑高手!!
  • #我与Java虚拟机的故事#连载17:我的Java技术水平有了一个本质的提升
  • (11)工业界推荐系统-小红书推荐场景及内部实践【粗排三塔模型】
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (附源码)计算机毕业设计ssm基于B_S的汽车售后服务管理系统
  • (剑指Offer)面试题34:丑数
  • (六)vue-router+UI组件库
  • (论文阅读11/100)Fast R-CNN
  • (译)计算距离、方位和更多经纬度之间的点
  • (转)【Hibernate总结系列】使用举例
  • .net CHARTING图表控件下载地址
  • .NET 分布式技术比较
  • .NET 指南:抽象化实现的基类
  • .set 数据导入matlab,设置变量导入选项 - MATLAB setvaropts - MathWorks 中国
  • [AHOI2009]中国象棋 DP,递推,组合数
  • [Angular 基础] - 表单:响应式表单
  • [Ariticle] 厚黑之道 一 小狐狸听故事
  • [BT]BUUCTF刷题第8天(3.26)
  • [C#] 基于 yield 语句的迭代器逻辑懒执行
  • [C#]winform部署yolov9的onnx模型
  • [C#]获取指定文件夹下的所有文件名(递归)
  • [C++] sqlite3_get_table 的使用
  • [C++]unordered系列关联式容器