【问题标题】:In WPF, how can I determine whether a control is visible to the user?在 WPF 中,如何确定控件是否对用户可见?
【发布时间】:2010-12-03 19:34:57
【问题描述】:

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

有什么方法可以确定控件是否对用户“可见”?

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

【问题讨论】:

  • 我曾经遇到过类似的问题。在编写代码来检测控件是否可见后,发现要检测的代码比实际更新隐藏控件要慢。对您的结果进行基准测试,因为它可能不值得。

标签: .net wpf user-interface wpf-controls visibility


【解决方案1】:

您可以使用我刚刚编写的这个小辅助函数来检查给定容器中的元素是否对用户可见。如果元素部分可见,则该函数返回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 您的窗口。

【讨论】:

  • 这不考虑元素超过容器大小的情况。返回 rect.IntersectsWith(bounds) 会解决这个问题。
  • 对于大量数据,您通常希望使用 UI 虚拟化。为此,您不直接设置您的项目(即ItemsContro.Items.Add(new ...)),而是使用数据绑定。但是,数据绑定会破坏视觉层次结构,因为添加到数据对象的子对象(例如ObservableList)没有父对象。 TransformToAncestor(或TransformToVisual)将不起作用。这种情况下我们该怎么办?!
【解决方案2】:
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);
}

【讨论】:

  • 这是否说明了由于窗口最小化或隐藏在其他窗口后面而导致元素不可见?
【解决方案3】:

接受的答案(以及此页面上的其他答案)解决了原始发布者遇到的具体问题,但他们没有对标题中写的问题给出充分的答案,即如何确定是否控件对用户可见。问题是 一个被其他控件覆盖的控件是不可见的,即使它可以呈现并且它位于其容器的边界内,这是其他答案正在解决的问题。

要确定控件是否对用户可见,您有时必须能够确定 WPF UIElement 是否可由用户点击(或在 PC 上鼠标可到达)

我在尝试检查按钮是否可以被用户单击鼠标时遇到了这个问题。困扰我的一个特殊情况是,一个按钮实际上对用户可见,但覆盖有一些透明(或半透明或完全不透明)层,以防止鼠标点击。在这种情况下,控件可能对用户可见但用户无法访问,这有点像它根本不可见。

所以我必须想出自己的解决方案。

编辑 - 我原来的帖子有一个使用 InputHitTest 方法的不同解决方案。然而,它在很多情况下都不起作用,我不得不重新设计它。这个解决方案更加健壮,并且似乎运行良好,没有任何假阴性或阳性。

解决方案:

  1. 获取对象相对于应用程序主窗口的绝对位置
  2. 请在所有角落(左上、左下、右上、右下)致电VisualTreeHelper.HitTest
  3. 如果从VisualTreeHelper.HitTest 获得的对象在其所有角上都等于原始对象或它的可视父对象,则我们将对象称为完全可点击,并且对于部分可点击一个或多个角。

请注意#1:完全可点击或部分可点击的定义 可点击并不准确 - 我们只是检查了所有四个角 对象是可点击的。例如,如果一个按钮有 4 个可点击的角,但它是 center 有一个不可点击的点,我们仍将其视为 完全可点击。检查给定对象中的所有点也是 浪费。

请注意#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
 }

【讨论】:

  • 我想试试这个,但我似乎缺少对 GetAbsolutePlacement() 和 FindVisualChildren() 的引用。我错过了什么?
  • 糟糕!我在以前的编辑中不小心删除了这些方法,现在它们又恢复了。谢谢!
  • 给出错误:“此 Visual 未连接到 PresentationSource。”
【解决方案4】:

将这些属性用于包含控件:

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

【讨论】:

  • Initialized 事件比这更合适。请注意,虚拟化可能会在您的对象可见之前初始化和连接您的对象,因此无论哪种方式,此方法都不能保证您的对象是可见的。
  • 以上链接失效。可以换个更新吗?谢谢!
猜你喜欢
  • 2011-07-30
  • 2013-02-10
  • 1970-01-01
  • 1970-01-01
  • 2011-03-17
  • 2013-03-26
  • 2011-03-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多