取消

准确判断一个 WPF 控件 / UI 元素当前是否显示在屏幕内

你的 WPF 窗口是可以拖到屏幕外面去的,所以拉几个元素到屏幕外很正常。你的屏幕可能有多个。你的多个屏幕可能有不同的 DPI。你检测的元素可能带有旋转。

各种各样奇怪的因素可能影响你检查此元素是否在屏幕内,本文包你一次性解决,绝对准确判断。


本文将说三种不同的判定方法,分偷懒版、日常版和苛刻版:

  • 如果你只是写个 demo 啥的,用偷懒版就够了,代码少性能高。
  • 如果你在项目/产品中使用,使用日常版就好。
  • 如果你的用户群体天天喷你 bug 多,那么用苛刻版更好。

偷懒版

如果你只想写个 demo,那么此代码足以。

判断 UI 元素的位置,其右侧是否在屏幕最左侧,其底部是否在屏幕最上面;或者其左侧是否在屏幕最右侧,其顶部是否在屏幕最下面。

偷懒版

1
2
3
4
5
6
7
8
9
private static bool IsOutsideOfScreen(FrameworkElement target)
{
    var topLeft = target.PointToScreen(new Point());
    var bottomRight = target.PointToScreen(new Point(target.ActualWidth, target.ActualHeight));
    return bottomRight.X < SystemParameters.VirtualScreenLeft
        || bottomRight.Y < SystemParameters.VirtualScreenTop
        || topLeft.X > SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth
        || topLeft.Y > SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight;
}

日常版(推荐)

如果你检测的元素自带了旋转,那么以上方法就不能准确判断了。

现在,我们需要检查这个元素的整个边界区域,即便是旋转后。于是,现在,我们要判断元素边界点所在的矩形区域了。

日常版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// <summary>
/// 判断一个可视化对象是否在屏幕外面无法被看见。
/// </summary>
/// <param name="target">要判断的可视化元素。</param>
/// <returns>如果元素在屏幕外面,则返回 true;如果元素在屏幕里或者部分在屏幕里面,则返回 false。</returns>
private static bool IsOutsideOfScreen(FrameworkElement target)
{
    try
    {
        var bounds = GetPixelBoundsToScreen(target);
        var screenBounds = GetScreenPixelBounds();
        var intersect = screenBounds;
        intersect.Intersect(bounds);
        return intersect.IsEmpty;
    }
    catch (InvalidOperationException)
    {
        // 此 Visual 未连接到 PresentationSource。
        return true;
    }

    Rect GetPixelBoundsToScreen(FrameworkElement visual)
    {
        var pixelBoundsToScreen = Rect.Empty;
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(0, 0)));
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(visual.ActualWidth, 0)));
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(0, visual.ActualHeight)));
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(visual.ActualWidth, visual.ActualHeight)));
        return pixelBoundsToScreen;
    }

    Rect GetScreenPixelBounds()
    {
        return new Rect(SystemParameters.VirtualScreenLeft, SystemParameters.VirtualScreenTop, SystemParameters.VirtualScreenWidth, SystemParameters.VirtualScreenHeight);
    }
}

苛刻版

现在,更复杂的场景来了。

如果用户有多台显示器,而且大小还不一样,那么依前面的判定方法,下图中 C 控件虽然人眼看在屏幕外,但计算所得是在屏幕内。

更复杂的,是多台显示器还不同 DPI 时,等效屏幕尺寸的计算更加复杂。更恐怖的是,WPF 程序声明支持的 DPI 级别不同,计算也会有一些差别。想要写一种支持所有支持级别的代码更加复杂。但本文可以。

苛刻版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// <summary>
/// 判断一个可视化对象是否在屏幕外面无法被看见。
/// </summary>
/// <param name="target">要判断的可视化元素。</param>
/// <returns>如果元素在屏幕外面,则返回 true;如果元素在屏幕里或者部分在屏幕里面,则返回 false。</returns>
private bool IsOutsideOfScreen(FrameworkElement target)
{
    var hwndSource = (HwndSource)PresentationSource.FromVisual(target);
    if (hwndSource is null)
    {
        return true;
    }
    var hWnd = hwndSource.Handle;
    var targetBounds = GetPixelBoundsToScreen(target);

    var screens = System.Windows.Forms.Screen.AllScreens;
    return !screens.Any(x => x.Bounds.IntersectsWith(targetBounds));

    System.Drawing.Rectangle GetPixelBoundsToScreen(FrameworkElement visual)
    {
        var pixelBoundsToScreen = Rect.Empty;
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(0, 0)));
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(visual.ActualWidth, 0)));
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(0, visual.ActualHeight)));
        pixelBoundsToScreen.Union(visual.PointToScreen(new Point(visual.ActualWidth, visual.ActualHeight)));
        return new System.Drawing.Rectangle(
            (int)pixelBoundsToScreen.X, (int)pixelBoundsToScreen.Y,
            (int)pixelBoundsToScreen.Width, (int)pixelBoundsToScreen.Height);
    }
}

在下面这段代码中,即便是 WPF 项目,我们也需要引用 Windows Forms,用于获取屏幕相关的信息。

如果是 SDK 风格的项目,则在 csproj 中添加如下代码:

1
2
3
4
5
6
7
8
9
10
    <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <UseWPF>true</UseWPF>
++      <UseWindowsForms>true</UseWindowsForms>
    </PropertyGroup>

    </Project>

如果是传统风格的项目,则直接添加 System.Windows.Forms 程序集的引用就好。

因为 WPF 的坐标单位是“设备无关单位”(我更倾向于叫有效像素,见 有效像素(Effective Pixels)),所以在系统对窗口有缩放行为的时候,多屏不同 DPI 的计算相当复杂,所以这里我们使用纯 Win32 / Windows Forms 方法在来计算屏幕与 UI 元素之间的交叉情况,并且避免在任何时候同时将多个屏幕的坐标进行加减乘除(避免单位不一致的问题)。所以这段代码对任何 WPF 的 DPI 配置都是有效且准确的。

关于 DPI 感知设置的问题,可阅读我的其他博客:

此代码的唯一的缺点是,在 WPF 项目里面要求引用 Windows Forms。

功能比较

不知道用哪个?看下表吧!

代码版本偷懒版日常版苛刻版
基础判断屏幕内外✔️✔️✔️
高分屏(非 96 DPI)✔️✔️✔️
整齐排列的多屏✔️✔️✔️
元素带有旋转✔️✔️
多屏尺寸不统一✔️
多屏有不同 DPI(WPF 感知系统 DPI)✔️
多屏有不同 DPI(WPF 感知屏幕 DPI)✔️
多屏有不同 DPI(WPF 感知屏幕 DPI V2)✔️
纯 WPF 代码(无需引用 Windows Forms)✔️✔️
元素形状不规则
性能较好一般

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/detect-whether-a-wpf-visual-is-inside-screen.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议

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

登录 GitHub 账号进行评论