简体   繁体   中英

How to scroll a DataGrid using scrollviewer of another DataGrid

I have two datagrids. Vertical scrollbar of first datagrid is hidden. Required scenario is that I want to make the first datagrid scroll its content whenever the second datagrid is scrolled. User cannot scroll the first datagrid manually but whenever the second datagrid is scrolled, the first datagrid should move in parallel with it.

I tried to change the value of vertical scrollbar of first datagrid as the value of second datagrid's vertical scrollbar changes but this simply changes the position of the scrollbar but does not scroll the content of the datagrid.

How to synchronize the scroll bar of first datagrid with the second one? It should look like as if both are a part of same UI element and thus scrollbar should ideally scroll both.

In WPF, I think you can use the method of:

DataGrid.ScrollIntoView(object item, DataGridColumn column);

to set the position of the DataGrid.

I guess you have two datagrids with exact the same number of items. I had something similar - sync two scrollviews. You probably need to create a behavior that synchronizes the VerticalOffset of the both scrollers. Here how i did:

XAML

<Window x:Class="Sandbox.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
   xmlns:behaviors="clr-namespace:Sandbox.Behaviors"
   Title="MainWindow" Height="350" Width="300"
>
   <Grid>
      <Grid.ColumnDefinitions>
         <ColumnDefinition Width="1*" />
         <ColumnDefinition Width="1*" />
      </Grid.ColumnDefinitions>
      <ScrollViewer CanContentScroll="True" Height="200" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Disabled">
         <i:Interaction.Behaviors>
            <behaviors:VerticalOffsetBehaviour Value="{Binding ElementName=otherScroller,Path=VerticalOffset}" />
         </i:Interaction.Behaviors>
         <TextBlock TextWrapping="Wrap">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque blandit, dolor vitae accumsan pellentesque, justo quam vehicula ante, vitae viverra enim nibh ac ex. Quisque efficitur lobortis lorem, id tempor nibh efficitur eget. Ut id felis enim. Aliquam commodo massa non dolor sollicitudin, pharetra bibendum turpis malesuada. Vestibulum vulputate blandit aliquam. Vestibulum ut varius mi. Phasellus ut massa turpis. In hac habitasse platea dictumst. Vestibulum efficitur elit et lobortis euismod. Mauris vitae ultricies velit.
         </TextBlock>
      </ScrollViewer>
      <ScrollViewer x:Name="otherScroller" Grid.Column="1" Height="200">
         <TextBlock TextWrapping="Wrap">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque blandit, dolor vitae accumsan pellentesque, justo quam vehicula ante, vitae viverra enim nibh ac ex. Quisque efficitur lobortis lorem, id tempor nibh efficitur eget. Ut id felis enim. Aliquam commodo massa non dolor sollicitudin, pharetra bibendum turpis malesuada. Vestibulum vulputate blandit aliquam. Vestibulum ut varius mi. Phasellus ut massa turpis. In hac habitasse platea dictumst. Vestibulum efficitur elit et lobortis euismod. Mauris vitae ultricies velit.
         </TextBlock>
      </ScrollViewer>
   </Grid>
</Window>

Behavior

using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace Sandbox.Behaviors
{
   public sealed class VerticalOffsetBehaviour : Behavior<ScrollViewer>
   {
      public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(VerticalOffsetBehaviour), new PropertyMetadata(VerticalOffsetBehaviour.OnValueChanged));

      public double Value
      {
         get { return (double)this.GetValue(ValueProperty); }
         set { this.SetValue(ValueProperty, value); }
      }

      private static void OnValueChanged(object source, DependencyPropertyChangedEventArgs args)
      {
         var behavior = (VerticalOffsetBehaviour)source;
         behavior.AssociatedObject.ScrollToVerticalOffset(behavior.Value);
      }
   }
}

You can use attached properties and styles to allow synchronization of scroll viewers. In the example below, I use one attached property to declare a synchronization scope à la Grid.IsSharedSizeScope , then use a style to set the IsSynchronized property on the DataGrid 's ScrollViewer .

SynchronizedScrollViewer.cs:

[Flags]
public enum SynchronizedScrollViewerMode
{
    Horizontal = 0x1,
    Vertical = 0x2,
    HorizontalAndVertical = Horizontal | Vertical,
    Disabled = 0
}

public sealed class SynchronizedScrollViewer : DependencyObject
{
    public SynchronizedScrollViewerMode Mode { get; }
    public VerticalAlignment VerticalAlignment { get; private set; }
    public HorizontalAlignment HorizontalAlignment { get; private set; }

    private class SyncrhonizedScrollViewerChild
    {
        public readonly ScrollViewer ScrollViewer;
        public bool IsDirty;

        public SyncrhonizedScrollViewerChild(ScrollViewer child)
        {
            if (child == null)
            {
                throw new ArgumentNullException(nameof(child));
            }

            this.ScrollViewer = child;
        }
    }

    private readonly List<SyncrhonizedScrollViewerChild> Children;

    public SynchronizedScrollViewer(SynchronizedScrollViewerMode mode)
    {
        if (mode == SynchronizedScrollViewerMode.Disabled)
        {
            throw new ArgumentNullException(nameof(mode));
        }

        this.Mode = mode;
        this.Children = new List<SyncrhonizedScrollViewerChild>();
    }

    #region Attached Properties

    public static SynchronizedScrollViewerMode GetScopeMode(DependencyObject obj)
    {
        return (SynchronizedScrollViewerMode)obj.GetValue(ScopeModeProperty);
    }

    public static void SetScopeMode(DependencyObject obj, SynchronizedScrollViewerMode value)
    {
        obj.SetValue(ScopeModeProperty, value);
    }

    public static readonly DependencyProperty ScopeModeProperty =
        DependencyProperty.RegisterAttached("ScopeMode", typeof(SynchronizedScrollViewerMode),
            typeof(SynchronizedScrollViewer), new PropertyMetadata(SynchronizedScrollViewerMode.Disabled));

    public static HorizontalAlignment GetHorizontalAlignment(DependencyObject obj)
    {
        return (HorizontalAlignment)obj.GetValue(HorizontalAlignmentProperty);
    }

    public static void SetHorizontalAlignment(DependencyObject obj, HorizontalAlignment value)
    {
        obj.SetValue(HorizontalAlignmentProperty, value);
    }

    public static readonly DependencyProperty HorizontalAlignmentProperty =
        DependencyProperty.RegisterAttached("HorizontalAlignment", typeof(HorizontalAlignment),
            typeof(SynchronizedScrollViewer), new PropertyMetadata(HorizontalAlignment.Left, Alignment_Changed));

    public static VerticalAlignment GetVerticalAlignment(DependencyObject obj)
    {
        return (VerticalAlignment)obj.GetValue(VerticalAlignmentProperty);
    }

    public static void SetVerticalAlignment(DependencyObject obj, VerticalAlignment value)
    {
        obj.SetValue(VerticalAlignmentProperty, value);
    }

    public static readonly DependencyProperty VerticalAlignmentProperty =
        DependencyProperty.RegisterAttached("VerticalAlignment", typeof(VerticalAlignment),
            typeof(SynchronizedScrollViewer), new PropertyMetadata(VerticalAlignment.Top, Alignment_Changed));

    public static bool GetIsSynchronized(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsSynchronizedProperty);
    }

    public static void SetIsSynchronized(DependencyObject obj, bool value)
    {
        obj.SetValue(IsSynchronizedProperty, value);
    }

    public static readonly DependencyProperty IsSynchronizedProperty =
        DependencyProperty.RegisterAttached("IsSynchronized", typeof(bool),
            typeof(SynchronizedScrollViewer), new FrameworkPropertyMetadata(false, IsSynchronized_Changed));

    public static SynchronizedScrollViewer GetScope(DependencyObject obj)
    {
        return (SynchronizedScrollViewer)obj.GetValue(ScopeProperty);
    }

    public static void SetScope(DependencyObject obj, SynchronizedScrollViewer value)
    {
        obj.SetValue(ScopeProperty, value);
    }

    public static readonly DependencyProperty ScopeProperty =
        DependencyProperty.RegisterAttached("Scope", typeof(SynchronizedScrollViewer),
            typeof(SynchronizedScrollViewer), new PropertyMetadata(null));

    #endregion

    private static void Alignment_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var scope = GetScope(d);
        if (scope == null)
        {
            // will be set later
        }
        else
        {
            scope.HorizontalAlignment = GetHorizontalAlignment(d);
            scope.VerticalAlignment = GetVerticalAlignment(d);
        }
    }

    private static void IsSynchronized_Changed(DependencyObject d, DependencyPropertyChangedEventArgs args)
    {
        var target = (ScrollViewer)d;
        var newValue = (bool)args.NewValue;

        if (newValue)
        {
            var scope = FindSynchronizationScope(target);
            scope.AddSynchronizedChild(target);
        }
    }

    private void AddSynchronizedChild(ScrollViewer target)
    {
        if (this.Children.Any(c => c.ScrollViewer == target))
        {
            throw new InvalidOperationException("Child is already synchronized");
        }

        this.Children.Add(new SyncrhonizedScrollViewerChild(target));
        target.ScrollChanged += Target_ScrollChanged;
    }

    private void Target_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        var sv = (ScrollViewer)sender;
        var child = Children.Single(s => s.ScrollViewer == sv);

        if (child.IsDirty)
        {
            // we just called "Set*Offset" on this child, so we don't wan't a loop
            // no-op
            child.IsDirty = false;
        }
        else
        {
            foreach (var otherChild in Children)
            {
                if (otherChild == child)
                {
                    // don't update the sender
                    continue;
                }

                var osv = otherChild.ScrollViewer;
                if (this.Mode.HasFlag(SynchronizedScrollViewerMode.Horizontal)
                    && otherChild.ScrollViewer.HorizontalOffset != child.ScrollViewer.HorizontalOffset)
                {
                    // already in sync
                    otherChild.IsDirty = true;
                    var targetOffset = sv.HorizontalOffset;
                    if (HorizontalAlignment == HorizontalAlignment.Center
                        || HorizontalAlignment == HorizontalAlignment.Stretch)
                    {
                        double scrollPositionPct = sv.HorizontalOffset / (sv.ExtentWidth - sv.ViewportWidth);
                        targetOffset = (osv.ExtentWidth - osv.ViewportWidth) * scrollPositionPct;
                    }
                    else if (HorizontalAlignment == HorizontalAlignment.Right)
                    {
                        targetOffset = otherChild.ScrollViewer.ExtentWidth - (sv.ExtentWidth - sv.HorizontalOffset);
                    }
                    otherChild.ScrollViewer.ScrollToHorizontalOffset(targetOffset);
                }

                if (this.Mode.HasFlag(SynchronizedScrollViewerMode.Vertical)
                    && otherChild.ScrollViewer.VerticalOffset != child.ScrollViewer.VerticalOffset)
                {
                    // already in sync
                    otherChild.IsDirty = true;
                    var targetOffset = sv.VerticalOffset;
                    if (VerticalAlignment == VerticalAlignment.Center
                        || VerticalAlignment == VerticalAlignment.Stretch)
                    {
                        double scrollPositionPct = sv.VerticalOffset / (sv.ExtentHeight - sv.ViewportHeight);
                        targetOffset = (osv.ExtentHeight - osv.ViewportHeight) * scrollPositionPct;
                    }
                    else if (VerticalAlignment == VerticalAlignment.Bottom)
                    {
                        targetOffset = otherChild.ScrollViewer.ExtentHeight - (sv.ExtentHeight - sv.VerticalOffset);
                    }
                    otherChild.ScrollViewer.ScrollToVerticalOffset(targetOffset);
                }
            }
        }
    }

    private static SynchronizedScrollViewer FindSynchronizationScope(ScrollViewer target)
    {
        for (DependencyObject obj = target; obj != null;
            // ContentPresenter seems to cause VisualTreeHelper to return null when FrameworkElement.Parent works.
            // http://stackoverflow.com/questions/6921881/frameworkelement-parent-and-visualtreehelper-getparent-behaves-differently
            obj = VisualTreeHelper.GetParent(obj) ?? (obj as FrameworkElement)?.Parent)
        {
            var mode = GetScopeMode(obj);
            if (mode != SynchronizedScrollViewerMode.Disabled)
            {
                var scope = GetScope(obj);
                if (scope == null)
                {
                    scope = new SynchronizedScrollViewer(mode);
                    scope.HorizontalAlignment = GetHorizontalAlignment(obj);
                    scope.VerticalAlignment = GetVerticalAlignment(obj);
                    SetScope(obj, scope);
                }
                return scope;
            }
        }

        throw new InvalidOperationException("A scroll viewer is set as synchronized, but no synchronization scope was found.");
    }
}

Example use:

<Grid local:SynchronizedScrollViewer.ScopeMode="HorizontalAndVertical" 
      local:SynchronizedScrollViewer.HorizontalAlignment="Center">
    <Grid.RowDefinitions>
        <RowDefinition Height="1*"/>
        <RowDefinition Height="1*"/>
    </Grid.RowDefinitions>

    <DataGrid Grid.Row="0" x:Name="dgTarget1" FrozenColumnCount="3">
        <DataGrid.Resources>
            <Style TargetType="{x:Type ScrollViewer}" BasedOn="{StaticResource {x:Type ScrollViewer}}">
                <Setter Property="local:SynchronizedScrollViewer.IsSynchronized" Value="True" />
            </Style>
        </DataGrid.Resources>
    </DataGrid>

    <DataGrid Grid.Row="1" x:Name="dgTarget2" FrozenColumnCount="3">
        <DataGrid.Resources>
            <Style TargetType="{x:Type ScrollViewer}" BasedOn="{StaticResource {x:Type ScrollViewer}}">
                <Setter Property="local:SynchronizedScrollViewer.IsSynchronized" Value="True" />
            </Style>
        </DataGrid.Resources>
    </DataGrid>
</Grid>

Examples of use with even and uneven content:

gif 显示两个列宽相同的数据网格的同步滚动 gif 显示两个不均匀滚动查看器的同步滚动

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM