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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1517743/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 13:24:45  来源:igfitidea点击:

In WPF, how can I determine whether a control is visible to the user?

.netwpfuser-interfacewpf-controlsvisibility

提问by Trap

I'm displaying a very big tree with a lot of items in it. Each of these items shows information to the user through its associated UserControl control, and this information has to be updated every 250 milliseconds, which can be a very expensive task since I'm also using reflection to access to some of their values. My first approach was to use the IsVisible property, but it doesn't work as I expected.

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

Is there any way I could determine whether a control is 'visible' to the user?

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

Note: I'm already using the IsExpanded property to skip updating collapsed nodes, but some nodes have 100+ elements and can't find a way to skip those which are outside the grid viewport.

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

回答by Julien Lebosquain

You can use this little helper function I just wrote that will check if an element is visible for the user, in a given container. The function returns trueif the element is partly visible. If you want to check if it's fully visible, replace the last line by rect.Contains(bounds).

您可以使用我刚刚编写的这个小辅助函数,该函数将检查给定容器中的元素是否对用户可见。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);
}

In your case, elementwill be your user control, and containeryour Window.

在您的情况下,element将是您的用户控件和container您的 Window。

回答by Andreas

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);
}

回答by Timoi

Use these properties for the containing control:

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

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

and then hook up listening to your data item's INotifyPropertyChanged.PropertyChanged subscribers like this

然后像这样监听数据项的 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");
        }
    }

For more detailed info see: http://joew.spaces.live.com/?_c11_BlogPart_BlogPart=blogview&_c=BlogPart&partqs=cat%3DWPF

有关更多详细信息,请参阅:http: //joew.spaces.live.com/?_c11_BlogPart_BlogPart=blogview&_c=BlogPart&partqs=cat%3DWPF

回答by Ofer Barasofsky

The accepted answer (and the other answers on this page) solve the specific problem that the original poster had but they don't give an adequate answer to the question written in the title, i.e., How to determine if a control is visible to the user. The problem is that A control that is covered by other controls is not visibleeven though it can be rendered and it is within the borders of its container which is what the other answers are solving for.

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

To determine whether a a control is visible to the user you sometimes have to be able to determine whether a WPF UIElement is Clickable (or mouse reachable on a PC) by the user

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

I encountered this problem when I was trying to check if a button can be mouse-clicked by the user. A special case scenario which bugged me was that a button can be actually visible to the user but covered with some transparent (or semi transparent or non transparent at all) layer that prevent mouse clicks. In such case a control might be visible to the user but not accessible to the user which is kind of like it is not visible at all.

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

So I had to come up with my own solution.

所以我不得不想出我自己的解决方案。

EDIT- My original post had a different solution that used InputHitTest method. However it didn't work in many cases and I had to redesign it. This solution is much more robust and seems to be working very well without any false negatives or positives.

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

Solution:

解决方案:

  1. Obtain object absolute position relative to the Application Main Window
  2. Call VisualTreeHelper.HitTeston all its corners (Top left, bottom left, top right, bottom right)
  3. We call an object Fully Clickableif the object obtained from VisualTreeHelper.HitTestequal the original object or a visual parent of it for all it's corners, and Partially Clickablefor one or more corners.
  1. 获取对象相对于应用程序主窗口的绝对位置
  2. 调用VisualTreeHelper.HitTest它的所有角落(左上角、左下角、右上角、右下角)
  3. 如果从其所有角落获得的对象与原始对象或其视觉父对象相同,则我们称该对象为完全可点击对象VisualTreeHelper.HitTest,而对于一个或多个角落则称为部分可点击

Please note #1: The definition here of Fully Clickable or Partially Clickable are not exact - we are just checking all four corners of an object are clickable. If, for example, a button has 4 clickable corners but it's center has a spot which is not clickable, we will still regard it as Fully Clickable. To check all points in a given object would be too wasteful.

Please note #2: it is sometimes required to set an object IsHitTestVisibleproperty to true(however, this is the default value for many common controls) if we wish VisualTreeHelper.HitTestto find it

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

Then all that is needed to find out if a button (for example) is clickable is to call:

然后,确定按钮(例如)是否可点击所需的一切就是调用:

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