开发者

Prevent Automatic Horizontal Scroll in TreeView

开发者 https://www.devze.com 2023-01-06 23:20 出处:网络
Whenever a node is selected in my treeview, it automatically does a horizontal scroll to that item.I开发者_高级运维s there a way to disable this?Handle the RequestBringIntoView event and set Handled t

Whenever a node is selected in my treeview, it automatically does a horizontal scroll to that item. I开发者_高级运维s there a way to disable this?


Handle the RequestBringIntoView event and set Handled to true, and the framework won't try to bring the item into view. For example, do something like this in your XAML:

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

And then this in your code-behind:

private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
    e.Handled = true;
}


I managed to solve the problem using the following:

<TreeView ScrollViewer.HorizontalScrollBarVisibility="Hidden">
   <TreeView.ItemsPanel>
      <ItemsPanelTemplate>
         <StackPanel MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ContentPresenter, AncestorLevel=1}}" />
      </ItemsPanelTemplate>
   </TreeView.ItemsPanel>
</TreeView>

I bind the width of the StackPanel which renders the ItemsPanel here, to the ActualWidth of ContentPresenter in the TreeView.

It also works nice with the "hacked" Stretching TreeView by: http://blogs.msdn.com/b/jpricket/archive/2008/08/05/wpf-a-stretching-treeview.aspx (I modified that solution not to remove grid column, but to change Grid.Column property of the first Decorator element from 1 to 2).


To offer a slightly simplified version of @lena's answer:

To scroll vertically while preserving the horizontal scroll position, and with no unwanted side effects, in the XAML, add event handlers for RequestBringIntoView and Selected:

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

In the code behind, add two event handlers:

private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
    // Ignore re-entrant calls
    if (mSuppressRequestBringIntoView)
        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.
    mSuppressRequestBringIntoView = true;

    TreeViewItem tvi = sender as TreeViewItem;
    if (tvi != null)
    {
        Rect newTargetRect = new Rect(-1000, 0, tvi.ActualWidth + 1000, tvi.ActualHeight);
        tvi.BringIntoView(newTargetRect);
    }

    mSuppressRequestBringIntoView = false;
}
private bool mSuppressRequestBringIntoView;

// Correctly handle programmatically selected items
private void OnSelected(object sender, RoutedEventArgs e)
{
    ((TreeViewItem)sender).BringIntoView();
    e.Handled = true;
}


Matthew, I manged to preserve vertical scrolling, and only prevent horizontal scrolling by restoring the horizontal position after a scroll caused by the RequestBringIntoView event .

private double treeViewHorizScrollPos = 0.0;
private bool treeViewResetHorizScroll = false;
private ScrollViewer treeViewScrollViewer = null;

private void TreeViewItemRequestBringIntoView( object sender, RequestBringIntoViewEventArgs e )
{
    if ( this.treeViewScrollViewer == null )
    {
        this.treeViewScrollViewer = this.DetailsTree.Template.FindName( "_tv_scrollviewer_", this.DetailsTree ) as ScrollViewer;
        if( this.treeViewScrollViewer != null )
            this.treeViewScrollViewer.ScrollChanged += new ScrollChangedEventHandler( this.TreeViewScrollViewerScrollChanged );
    }
    this.treeViewResetHorizScroll = true;
    this.treeViewHorizScrollPos = this.treeViewScrollViewer.HorizontalOffset;
}

private void TreeViewScrollViewerScrollChanged( object sender, ScrollChangedEventArgs e )
{
    if ( this.treeViewResetHorizScroll )
        this.treeViewScrollViewer.ScrollToHorizontalOffset( this.treeViewHorizScrollPos );

    this.treeViewResetHorizScroll = false;
}


I had a similar problem. I needed to prevent horizontal scroll but preserve vertical scroll. My solution is to handle OnRequestBringIntoView method as I want it to behave. I created a ResourceDictionary for a TreeViewItem and added EventSetters for OnSelected and OnRequestBringIntoView methods.

MyResourceDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  x:Class="Resources.MyResourceDictionary" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style TargetType="TreeViewItem" x:Key="treeitem" >
        <EventSetter Event="RequestBringIntoView"  Handler="OnRequestBringIntoView"/>
        <EventSetter Event="Selected" Handler="OnSelected"/>
    </Style>
</ResourceDictionary>

MyResourceDictionary.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace Resources
{
    partial class MyResourceDictionary:ResourceDictionary
    {
        public MyResourceDictionary()
        {
            InitializeComponent();
        }

        private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
        {
            e.Handled = true; //prevent event bubbling
            var item = (TreeViewItem)sender;
            TreeView tree = GetParentTree(item) as TreeView;
            if(tree!=null)
            {
                var scrollViewer = tree.Template.FindName("_tv_scrollviewer_", tree) as ScrollViewer;
                if (scrollViewer != null)
                {
                    scrollViewer.ScrollToLeftEnd();//prevent horizontal scroll
                    Point relativePoint = item.TransformToAncestor(tree).Transform(new Point(0, 0));//get position of a selected item 
                    if (relativePoint.Y <= scrollViewer.ContentVerticalOffset) return;//do no scroll if we select inside one 'scroll screen'
                    scrollViewer.ScrollToVerticalOffset(relativePoint.Y);//scroll to Y of a selected item
                }
            }
        }

        private DependencyObject GetParentTree(DependencyObject item)
        {
            var target = VisualTreeHelper.GetParent(item);
            return target as TreeView != null ? target : GetParentTree(target);
        }

        private void OnSelected(object sender, RoutedEventArgs e) //handle programmatically selected items
        {
            var item = (TreeViewItem)sender;
            item.BringIntoView();
            e.Handled = true;
        }
    }
}


Following solution is more simple and fully tested and more compatible, You don't need to calculate and change scrollbar offset, what you need is moving horizontal scrollbar to left, since "RequestBringIntoView" event routing strategy is bubbling, you simply need to do it on last item reached event. Name scrollViewer control "_tv_scrollviewer_"

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

On code behind:

    private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
    {
        var item = (TreeViewItem)sender;
        if (item != null)
        {
            // move horizontal scrollbar only when event reached last parent item
            if (item.Parent == null)
            {
                var scrollViewer = itemsTree.Template.FindName("_tv_scrollviewer_", itemsTree) as ScrollViewer;
                if (scrollViewer != null)
                    Dispatcher.BeginInvoke(DispatcherPriority.Loaded, (Action)(() => scrollViewer.ScrollToLeftEnd()));
            }
        }

    }


@lena's solution of preserving vertical scrolling worked best for me. I've iterated on it a little bit:

    private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
    {
        var treeViewItem = (TreeViewItem)sender;
        var scrollViewer = treeView.Template.FindName("_tv_scrollviewer_", treeView) as ScrollViewer;

        Point topLeftInTreeViewCoordinates = treeViewItem.TransformToAncestor(treeView).Transform(new Point(0, 0));
        var treeViewItemTop = topLeftInTreeViewCoordinates.Y;
        if (treeViewItemTop < 0
            || treeViewItemTop + treeViewItem.ActualHeight > scrollViewer.ViewportHeight
            || treeViewItem.ActualHeight > scrollViewer.ViewportHeight)
        {
            // if the item is not visible or too "tall", don't do anything; let them scroll it into view
            return;
        }

        // if the item is already fully within the viewport vertically, disallow horizontal scrolling
        e.Handled = true;
    }

What this does is let the ScrollViewer scroll normally if the item isn't already in the viewport vertically. However for the actual "annoying" case (where the item is already visible), it sets e.Handled to true, thus preventing horizontal scrolling.


I had a DataGrid that I wanted to do the same operation on and used POHB's answer mostly. I had to modify it for my solution. The code is shown below. The datagrid is a 2 x 2 datagrid with the first column being thin and the second being very wide (1000+). The first column is frozen. I hope this helps someone out. -Matt

  public partial class MyUserControl : UserControl
  {
      private ScrollContentPresenter _scrollContentPresenter;
      private ScrollViewer _scrollViewer;
      private double _dataGridHorizScrollPos = 0.0;
      private bool _dataGridResetHorizScroll = false;

      public MyUserControl()
      {
          // setup code...
          _dataGrid.ApplyTemplate();

          _scrollViewer = FindVisualChild<ScrollViewer>(_dataGrid);
          _scrollViewer.ScrollChanged += new ScrollChangedEventHandler(DataGridScrollViewerScrollChanged);

          _scrollContentPresenter = FindVisualChild<ScrollContentPresenter>(_scrollViewer);
          _scrollContentPresenter.RequestBringIntoView += new RequestBringIntoViewEventHandler(_scrollContentPresenter_RequestBringInputView);              
      }

      private void DataGridScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e)
      {
          if (_dataGridResetHorizScroll)
          {
              _scrollViewer.ScrollToHorizontalOffset(_dataGridHorizScrollPos);
          }
          // Note: When the row just before a page change is selected and then the next row on the  
          // next page is selected, a second event fires setting the horizontal offset to 0
          // I'm ignoring those large changes by only recording the offset when it's large. -MRB
          else if (Math.Abs(e.HorizontalChange) < 100)
          {
              _dataGridHorizScrollPos = _scrollViewer.HorizontalOffset;
          }

          _dataGridResetHorizScroll = false;
      }

      public T FindVisualChild<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 ScrollViewer))
                  {
                      // I needed this since the template wasn't applied yet when 
                      // calling from  the constructor
                      (child as ScrollViewer).ApplyTemplate();
                  }

                  if (child != null && child is T)
                  {
                      return (T)child;
                  }

                  T childItem = FindVisualChild<T>(child);
                  if (childItem != null) return childItem;
              }
          }
          return null;
      }

      private void _scrollContentPresenter_RequestBringInputView(object sender, RequestBringIntoViewEventArgs e)
      {
          _dataGridResetHorizScroll = true;
      }
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号