在WPF中,我如何确定控件对用户是否可见?

70

我正在显示一个非常大的树,其中有很多项。每个项目通过其关联的UserControl控件向用户显示信息,并且这些信息必须每250毫秒更新一次,这可能是一项非常昂贵的任务,因为我还要使用反射来访问其中一些值。我的第一种方法是使用IsVisible属性,但它并不像我预期的那样工作。

有没有办法确定控件对用户是否“可见”?

注意:我已经使用IsExpanded属性跳过更新折叠节点,但有些节点有100多个元素,无法找到跳过网格视口之外元素的方法。


6
我曾经遇到过类似的问题。编写代码来检测控件是否可见后,结果发现检测代码比实际更新隐藏控件更慢。对你的结果进行基准测试,因为这样做可能并不值得。 - Andrew Keith
4个回答

93
你可以使用我刚写的这个小帮助函数,它将检查给定容器中的元素是否对用户可见。如果元素部分可见,则函数返回true。如果要检查元素是否完全可见,请将最后一行替换为rect.Contains(bounds)
private bool IsUserVisible(FrameworkElement element, FrameworkElement container)
{
    if (!element.IsVisible)
        return false;

    Rect bounds = element.TransformToAncestor(container).TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
    Rect rect = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
    return rect.Contains(bounds.TopLeft) || rect.Contains(bounds.BottomRight);
}
在你的情况下,element将是你的用户控件,而container则是你的窗口。

30
如果元素超过容器的大小,这将不予考虑。返回rect.IntersectsWith(bounds)可以解决这个问题。 - Amanduh
5
处理大量数据时,通常需要使用UI虚拟化。为此,您不会直接设置项目(即ItemsContro.Items.Add(new...)),而是使用数据绑定。 然而,数据绑定会破坏可视化层次结构,因为添加到数据对象(例如ObservableList)中的子元素将没有父级。 TransformToAncestor(或TransformToVisual)将无法正常工作。在这种情况下,我们该怎么办呢?! - Shakaron
我不得不在IsVisible检查后添加“if (element.RenderSize.Height == 0) return false;”,以使它在我的情况下正常工作。 - sarh

21
public static bool IsUserVisible(this UIElement element)
{
    if (!element.IsVisible)
        return false;
    var container = VisualTreeHelper.GetParent(element) as FrameworkElement;
    if (container == null) throw new ArgumentNullException("container");

    Rect bounds = element.TransformToAncestor(container).TransformBounds(new Rect(0.0, 0.0, element.RenderSize.Width, element.RenderSize.Height));
    Rect rect = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
    return rect.IntersectsWith(bounds);
}

这是否考虑到元素因窗口最小化或隐藏在其他窗口后而无法看到的情况? - Wobbles
这只是已接受答案的稍微修改版本,但更适合复制粘贴,所以请为两者点赞。并且在评论中提到,我不得不在容器为空检查后添加“if (element.RenderSize.Height == 0) return false;”才能使其在我的情况下正常工作。 - sarh

12
接受的回答(以及本页上的其他答案)解决了原帖作者的具体问题,但它们并没有给出一个充分的回答来回答标题中的问题,即“如何确定控件对用户是否可见”。问题在于,即使被覆盖的控件可以被呈现并位于其容器的边界内(这正是其他答案要解决的问题),但被其他控件遮盖的控件是不可见的
为了确定一个控件对用户是否可见,有时需要能够确定 WPF UIElement 是否可点击(或在 PC 上是否可由鼠标到达)
当我想检查一个按钮是否可以被用户用鼠标点击时,我遇到了这个问题。困扰我的一个特殊情况是,一个按钮实际上可以对用户可见,但却被一些透明(或半透明或完全不透明)层覆盖,防止鼠标点击。在这种情况下,一个控件可能对用户可见,但用户无法访问它,这有点像它根本不可见。
因此,我必须想出自己的解决方案。
编辑-我的原始帖子有一个使用InputHitTest方法的不同解决方案。然而,在许多情况下它并不起作用,我不得不重新设计它。这个解决方案更加健壮,并且似乎非常好地工作,没有任何错误的负面或正面。 解决方案:
  1. 获取对象相对于应用程序主窗口的绝对位置
  2. 在所有四个角(左上,左下,右上,右下)上调用VisualTreeHelper.HitTest
  3. 如果从VisualTreeHelper.HitTest获得的对象等于原始对象或其视觉父对象,我们将称之为完全可点击的对象,对于一个或多个角落而言, 称之为部分可点击
请注意#1:此处关于“完全可点击”或“部分可点击”的定义并不精确-我们只检查一个对象的所有四个角是否可点击。例如,如果一个按钮有4个可点击的角落,但其中心有一个不可点击的点,我们仍将把它视为“完全可点击”。要检查给定对象中的所有点将太浪费时间。
请注意#2:有时需要将一个对象的“IsHitTestVisible”属性设置为true(然而,对于许多常见的控件来说,这是默认值),如果我们希望“VisualTreeHelper.HitTest”找到它。
    private bool isElementClickable<T>(UIElement container, UIElement element, out bool isPartiallyClickable)
    {
        isPartiallyClickable = false;
        Rect pos = GetAbsolutePlacement((FrameworkElement)container, (FrameworkElement)element);
        bool isTopLeftClickable = GetIsPointClickable<T>(container, element, new Point(pos.TopLeft.X + 1,pos.TopLeft.Y+1));
        bool isBottomLeftClickable = GetIsPointClickable<T>(container, element, new Point(pos.BottomLeft.X + 1, pos.BottomLeft.Y - 1));
        bool isTopRightClickable = GetIsPointClickable<T>(container, element, new Point(pos.TopRight.X - 1, pos.TopRight.Y + 1));
        bool isBottomRightClickable = GetIsPointClickable<T>(container, element, new Point(pos.BottomRight.X - 1, pos.BottomRight.Y - 1));

        if (isTopLeftClickable || isBottomLeftClickable || isTopRightClickable || isBottomRightClickable)
        {
            isPartiallyClickable = true;
        }

        return isTopLeftClickable && isBottomLeftClickable && isTopRightClickable && isBottomRightClickable; // return if element is fully clickable
    }

    private bool GetIsPointClickable<T>(UIElement container, UIElement element, Point p) 
    {
        DependencyObject hitTestResult = HitTest< T>(p, container);
        if (null != hitTestResult)
        {
            return isElementChildOfElement(element, hitTestResult);
        }
        return false;
    }               

    private DependencyObject HitTest<T>(Point p, UIElement container)
    {                       
        PointHitTestParameters parameter = new PointHitTestParameters(p);
        DependencyObject hitTestResult = null;

        HitTestResultCallback resultCallback = (result) =>
        {
           UIElement elemCandidateResult = result.VisualHit as UIElement;
            // result can be collapsed! Even though documentation indicates otherwise
            if (null != elemCandidateResult && elemCandidateResult.Visibility == Visibility.Visible) 
            {
                hitTestResult = result.VisualHit;
                return HitTestResultBehavior.Stop;
            }

            return HitTestResultBehavior.Continue;
        };

        HitTestFilterCallback filterCallBack = (potentialHitTestTarget) =>
        {
            if (potentialHitTestTarget is T)
            {
                hitTestResult = potentialHitTestTarget;
                return HitTestFilterBehavior.Stop;
            }

            return HitTestFilterBehavior.Continue;
        };

        VisualTreeHelper.HitTest(container, filterCallBack, resultCallback, parameter);
        return hitTestResult;
    }         

    private bool isElementChildOfElement(DependencyObject child, DependencyObject parent)
    {
        if (child.GetHashCode() == parent.GetHashCode())
            return true;
        IEnumerable<DependencyObject> elemList = FindVisualChildren<DependencyObject>((DependencyObject)parent);
        foreach (DependencyObject obj in elemList)
        {
            if (obj.GetHashCode() == child.GetHashCode())
                return true;
        }
        return false;
    }

    private IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                if (child != null && child is T)
                {
                    yield return (T)child;
                }

                foreach (T childOfChild in FindVisualChildren<T>(child))
                {
                    yield return childOfChild;
                }
            }
        }
    }

    private Rect GetAbsolutePlacement(FrameworkElement container, FrameworkElement element, bool relativeToScreen = false)
    {
        var absolutePos = element.PointToScreen(new System.Windows.Point(0, 0));
        if (relativeToScreen)
        {
            return new Rect(absolutePos.X, absolutePos.Y, element.ActualWidth, element.ActualHeight);
       }
        var posMW = container.PointToScreen(new System.Windows.Point(0, 0));
        absolutePos = new System.Windows.Point(absolutePos.X - posMW.X, absolutePos.Y - posMW.Y);
        return new Rect(absolutePos.X, absolutePos.Y, element.ActualWidth, element.ActualHeight);
   }

如果想要知道一个按钮是否可点击,只需调用以下代码:

 if (isElementClickable<Button>(Application.Current.MainWindow, myButton, out isPartiallyClickable))
 {
      // Whatever
 }

1
我想尝试一下,但是看起来我缺少对GetAbsolutePlacement()和FindVisualChildren()的引用。我错过了什么? - Rick Morgan
糟糕!我在之前的编辑中不小心删除了那些方法,现在它们已经恢复了。谢谢! - Ofer Barasofsky
出现错误:'此可视化未连接到 PresentationSource。' - user1034912
1
谢谢。这比被接受的答案可靠多了! - Smolakian
非常感谢您提供的解决更广泛问题的答案。我之前不知道有 VisualTreeHelper.HitTest 这个方法,真是个绝妙的想法! - undefined

5

使用以下属性控制容器:

VirtualizingStackPanel.IsVirtualizing="True" 
VirtualizingStackPanel.VirtualizationMode="Recycling"

然后,可以像这样连接到数据项的INotifyPropertyChanged.PropertyChanged订阅者并监听它们的变化。
    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            Console.WriteLine(
               "WPF is listening my property changes so I must be visible");
        }
        remove
        {
            Console.WriteLine("WPF unsubscribed so I must be out of sight");
        }
    }

更详细的信息请参见: http://joew.spaces.live.com/?_c11_BlogPart_BlogPart=blogview&_c=BlogPart&partqs=cat%3DWPF

(此链接为相关的IT技术内容,如需了解详情,请点击链接)

2
Initialized事件比这个方法更加合适。请注意,虚拟化可能会在对象可见之前初始化和连接它,因此无论哪种方式,该方法都不能保证您的对象是可见的。 - Doug
1
以上链接已经失效了。你能否更新一个替代链接?谢谢! - John Beyer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接