你的 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 感知设置的问题,可阅读我的其他博客:
- Windows 下的高 DPI 应用开发(UWP / WPF / Windows Forms / Win32) - walterlv
- 支持 Windows 10 最新 PerMonitorV2 特性的 WPF 多屏高 DPI 应用开发 - walterlv
- Windows 系统上使用任务管理器查看进程的各项属性(命令行、DPI、管理员权限等) - walterlv
此代码的唯一的缺点是,在 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]) 。