In WPF, how can I determine whether a control is visible to the user?
I'm displaying a very big tree with a lot of items in it. Each of these items shows inf开发者_运维问答ormation 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.
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.
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);
}
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 wishVisualTreeHelper.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
}
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
精彩评论