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

4

72

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.

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.

Disproof answered 4/10, 2009 at 23:55 Comment(1)
I once had a similar problem. After writing code to detect if a control is visible, it turned out that the code to detect was slower than actually updating the hidden control. Benchmark your results because it might not be worth it.Morn
W
94

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 true if the element is partly visible. If you want to check if it's fully visible, replace the last line by 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, element will be your user control, and container your Window.

Wurth answered 5/10, 2009 at 0:40 Comment(3)
This does not account for the case where the element exceeds the size of the container. Returning rect.IntersectsWith(bounds) instead will fix that.Telephone
With large number of data you typically want to use UI Virtualization. For that, you don't set your items directly (i.e. ItemsContro.Items.Add(new ...)) but rather use data binding. However, data binding will break the visual hierarchy as the children added to your data object (e.g. ObservableList) won't have a parent. TransformToAncestor (or TransformToVisual) will not work. What shall we do in this case?!Judyjudye
I had to add "if (element.RenderSize.Height == 0) return false;" after IsVisible check to make it work properly for my caseSerpens
W
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);
}
Wolfy answered 28/1, 2014 at 17:4 Comment(2)
Does this account for the element not being seen due to window minimized or hidden behind other windows?Weswesa
Just a little bit modified version of accepted answer but more copy-paste-usable so upvote for both. And commenting to both that I had to add "if (element.RenderSize.Height == 0) return false;" after container null check to make it work properly for my caseSerpens
T
13

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 visible even 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

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.

Solution:

  1. Obtain object absolute position relative to the Application Main Window
  2. Call VisualTreeHelper.HitTest on all its corners (Top left, bottom left, top right, bottom right)
  3. We call an object Fully Clickable if the object obtained from VisualTreeHelper.HitTest equal the original object or a visual parent of it for all it's corners, and Partially Clickable for one or more corners.

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 IsHitTestVisible property to true (however, this is the default value for many common controls) if we wish VisualTreeHelper.HitTest to find it

    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
 }
Transposition answered 15/2, 2017 at 16:28 Comment(5)
I'd like to try this out, but it looks like I'm missing references to GetAbsolutePlacement() and FindVisualChildren(). What am I missing?Concordat
Oops! I've accidentally deleted those methods on previous edits, now they are back on. Thanks!Transposition
Gives an error: 'This Visual is not connected to a PresentationSource.'Yorker
Thanks. This was much more reliable than the accepted answer!Cathryncathy
Thank you for this answer that actually solves the broader issue. Didn't know about VisualTreeHelper.HitTest, what a brilliant idea!Funeral
T
5

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

    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

Tubulure answered 4/12, 2009 at 10:20 Comment(2)
The Initialized event is much more appropriate than this. Note that virtualization may initialize and wireup your object much earlier than it is visible, so either way, this method doesn't guarantee that your object is visible.Drill
The above link is broken. Can you update with a replacement? Thanks!Videogenic

© 2022 - 2024 — McMap. All rights reserved.