WPF TreeView - How to scroll so expanded branch is

2019-01-22 18:31发布

问题:

When I expand items in my treeview so that scrolling is necessary, a scrollbar appears. However, it doesn't scroll down for the newly expanded branch of items - they get cropped by the bottom of the control. So as I continue expanding items at the bottom of the tree, I have to keep manually scrolling down to see the new children. Anyone have a suggestion for how make it automatically scroll to show the newly expanded items?

回答1:

On the TreeView, handle the TreeViewItem.Expanded event (you can do this at the TreeView level because of event bubbling). In the Expanded handler, call BringIntoView on the TreeViewItem that raised the event.

You may need a bit of trial and error to get hold of the TreeViewItem in your event handler code. I think (haven't checked) that the sender argument to your Expanded event handler will be the TreeView (since that's where the event handler is attached) rather than the TreeViewItem. And the e.Source or e.OriginalSource may be an element in the TreeViewItem's data template. So you may need to use VisualTreeHelper to walk up the visual tree to find the TreeViewItem. But if you use the debugger to inspect the sender and the RoutedEventArgs this should be trivial to figure out.

(If you're able to get this working and want to bundle it up so you don't have to attach the same event handler to every TreeView, it should be easy to encapsulate it as an attached behaviour which will allow you to apply it declaratively, including via a Style.)



回答2:

You can use a simple EventSetter in TreeViewItem style to invoke an event handler when the item is selected. Then call BringIntoView for the item.

<TreeView >
 <TreeView.ItemContainerStyle>
   <Style TargetType="{x:Type TreeViewItem}">
     <EventSetter Event="Selected" Handler="TreeViewSelectedItemChanged" />
   </Style>
 </TreeView.ItemContainerStyle>

</TreeView>

private void TreeViewSelectedItemChanged(object sender, RoutedEventArgs e)
{
    TreeViewItem item = sender as TreeViewItem;
    if (item != null)
    {
        item.BringIntoView();
        e.Handled = true;  
    }
}


回答3:

Use a dependency property on an IsSelected trigger:

<Style TargetType="{x:Type TreeViewItem}">
 <Style.Triggers>
  <Trigger Property="IsSelected" Value="True">
    <Setter Property="commands:TreeViewItemBehavior.BringIntoViewWhenSelected" Value="True" />
  </Trigger>
</Style.Triggers>

Here's the code for the dependency property:

public static bool GetBringIntoViewWhenSelected(TreeViewItem treeViewItem)
{
  return (bool)treeViewItem.GetValue(BringIntoViewWhenSelectedProperty);
}

public static void SetBringIntoViewWhenSelected(TreeViewItem treeViewItem, bool value)
{
  treeViewItem.SetValue(BringIntoViewWhenSelectedProperty, value);
}

public static readonly DependencyProperty BringIntoViewWhenSelectedProperty =
    DependencyProperty.RegisterAttached("BringIntoViewWhenSelected", typeof(bool),
    typeof(TreeViewItemBehavior), new UIPropertyMetadata(false, OnBringIntoViewWhenSelectedChanged));

static void OnBringIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
  TreeViewItem item = depObj as TreeViewItem;
  if (item == null)
    return;

  if (e.NewValue is bool == false)
    return;

  if ((bool)e.NewValue)
    item.BringIntoView();
}


回答4:

Thanks to itowlson's answer, here's the expanded event handler code that works for both of my trees

private static void Tree_Expanded(object sender, RoutedEventArgs e)
{
    // ignore checking, assume original source is treeviewitem
    var treeViewItem = (TreeViewItem)e.OriginalSource;

    var count = VisualTreeHelper.GetChildrenCount(treeViewItem);

    for (int i = count - 1; i >= 0; --i)
    {
        var childItem = VisualTreeHelper.GetChild(treeViewItem, i);
        ((FrameworkElement)childItem).BringIntoView();
    }

    // do NOT call BringIntoView on the actual treeviewitem - this negates everything
    //treeViewItem.BringIntoView();
}


回答5:

I modified Jared's answer in combination with the strategy from here: https://stackoverflow.com/a/42238409/2477582

The main advantage is that there aren't n calls of BringIntoView() for n childs. There is only one call of BringIntoView for an area that covers all of the child's heights.

Additionally, the purpose of the referred topic is realized as well. But this part may be removed, if unwanted.

/// <summary>Prevents automatic horizontal scrolling, while preserving automatic vertical scrolling and other side effects</summary>
/// <remarks>Source: https://stackoverflow.com/a/42238409/2477582 </remarks>
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
    // Ignore re-entrant calls
    if (m_SuppressRequestBringIntoView)
        return;

    // Cancel the current scroll attempt
    e.Handled = true;

    // Call BringIntoView using a rectangle that extends into "negative space" to the left of our
    // actual control. This allows the vertical scrolling behaviour to operate without adversely
    // affecting the current horizontal scroll position.
    m_SuppressRequestBringIntoView = true;

    try
    {
        TreeViewItem tvi = sender as TreeViewItem;
        if (tvi != null)
        {
            // take care of children
            int ll_ChildCount = VisualTreeHelper.GetChildrenCount(tvi);
            double ll_Height = tvi.ActualHeight;

            if (ll_ChildCount > 0)
            {
                FrameworkElement ll_LastChild = VisualTreeHelper.GetChild(tvi, ll_ChildCount - 1) as FrameworkElement;
                ll_Height += ll_ChildCount * ll_LastChild.ActualHeight;
            }

            Rect newTargetRect = new Rect(-1000, 0, tvi.ActualWidth + 1000, ll_Height);
            tvi.BringIntoView(newTargetRect);
        }
    }
    catch (Exception ex)
    {
        m_Log.Debug("Error in TreeViewItem_RequestBringIntoView: " + ex.ToString());
    }

    m_SuppressRequestBringIntoView = false;
}

The above solution works together with this:

/// <summary>Correctly handle programmatically selected items (needed due to the custom implementation of TreeViewItem_RequestBringIntoView)</summary>
/// <remarks>Source: https://stackoverflow.com/a/42238409/2477582 </remarks>
private void TreeViewItem_Selected(object sender, RoutedEventArgs e)
{
    ((TreeViewItem)sender).BringIntoView();

    e.Handled = true;
}

This part takes care of toggling the elements at each click:

/// <summary>Support for single click toggle</summary>
private void TreeViewItem_MouseUp(object sender, MouseButtonEventArgs e)
{
    TreeViewItem tvi = null;

    // Source may be TreeViewItem directly, or be a ContentPresenter
    if (e.Source is TreeViewItem)
    {
        tvi = e.Source as TreeViewItem;
    }
    else if (e.Source is ContentPresenter)
    {
        tvi = (e.Source as ContentPresenter).TemplatedParent as TreeViewItem;
    }

    if (tvi == null || e.Handled) return;

    tvi.IsExpanded = !tvi.IsExpanded;
    e.Handled = true;
}

Finally the XAML part:

<TreeView>
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView" />
            <EventSetter Event="Selected" Handler="TreeViewItem_Selected" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>