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:
- Obtain object absolute position relative to the Application Main Window
- Call
VisualTreeHelper.HitTest
on all its corners (Top left, bottom left, top right, bottom right)
- 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
}