WPF中,当任意一个ScrollView被滚动时,两个ScrollView应进行同步滚动。

24
我已经阅读了这个帖子:

binding two VerticalScrollBars one to another

,它几乎帮助实现了目标,但仍有一些缺失。问题在于,将滚动条左右或上下移动时,在我的两个滚动视图中都会按预期滚动,但是当我们尝试使用/单击这些滚动条末端的箭头按钮进行滚动时,只有一个滚动视图被滚动,这不是预期的行为。那么我们需要添加/编辑什么来解决这个问题呢?
5个回答

48
一种方法是使用“ScrollChanged”事件来更新其他“ScrollViewer”。
<ScrollViewer Name="sv1" Height="100" 
              HorizontalScrollBarVisibility="Auto"
              ScrollChanged="ScrollChanged">
    <Grid Height="1000" Width="1000" Background="Green" />
</ScrollViewer>

<ScrollViewer Name="sv2" Height="100" 
              HorizontalScrollBarVisibility="Auto"
              ScrollChanged="ScrollChanged">
    <Grid Height="1000" Width="1000" Background="Blue" />
</ScrollViewer>

private void ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (sender == sv1)
        {
            sv2.ScrollToVerticalOffset(e.VerticalOffset);
            sv2.ScrollToHorizontalOffset(e.HorizontalOffset);
        }
        else
        {
            sv1.ScrollToVerticalOffset(e.VerticalOffset);
            sv1.ScrollToHorizontalOffset(e.HorizontalOffset);
        }
    }

1
修改您的代码:当(sender == sv1)为假时,将sv2.ScrollToHorizontalOffset更改为sv1.ScrollToHorizontalOffset - Epsil0neR
谢谢!就是这样了..!现在一切都符合预期的行为。 - Vikram_
5
为了避免在此事件处理程序被调用时(例如视口大小更改、范围更改等),由于其他原因导致它们不断重置为0,我不得不添加if (e.VerticalChange == 0 && e.HorizontalChange == 0) { return; }。请注意,该语句的目的是保持原有功能,同时避免重复设置。 - E-rich

11

这个问题是针对WPF的,但如果有任何开发UWP应用程序的人不小心遇到了这个问题,我需要采取稍微不同的方法。
在UWP中,当您设置另一个滚动视图的滚动偏移量(使用ScrollViewer.ChangeView)时,它也会触发另一个滚动视图的ViewChanged事件,从而创建一个循环,导致它非常卡顿,并且无法正常工作。

我通过在处理事件时使用少量超时来解决这个问题,如果被滚动的对象不等于最后一个处理该事件的对象。

XAML:

<ScrollViewer x:Name="ScrollViewer1" ViewChanged="SynchronizedScrollerOnViewChanged"> ... </ScrollViewer>
<ScrollViewer x:Name="ScrollViewer2" ViewChanged="SynchronizedScrollerOnViewChanged"> ... </ScrollViewer>

后台代码:

public sealed partial class MainPage
{
    private const int ScrollLoopbackTimeout = 500;

    private object _lastScrollingElement;
    private int _lastScrollChange = Environment.TickCount;

    public SongMixerUserControl()
    {
        InitializeComponent();
    }

    private void SynchronizedScrollerOnViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
    {
        if (_lastScrollingElement != sender && Environment.TickCount - _lastScrollChange < ScrollLoopbackTimeout) return;

        _lastScrollingElement = sender;
        _lastScrollChange = Environment.TickCount;

        ScrollViewer sourceScrollViewer;
        ScrollViewer targetScrollViewer;
        if (sender == ScrollViewer1)
        {
            sourceScrollViewer = ScrollViewer1;
            targetScrollViewer = ScrollViewer2;
        }
        else
        {
            sourceScrollViewer = ScrollViewer2;
            targetScrollViewer = ScrollViewer1;
        }

        targetScrollViewer.ChangeView(null, sourceScrollViewer.VerticalOffset, null);
    }
}

请注意,超时时间为500毫秒。这可能看起来有点长,但是由于UWP应用程序在滚动时具有动画效果(或者确切地说是缓动),使用鼠标滚轮时会导致事件在几百毫秒内触发多次。这个超时时间似乎完美地解决了这个问题。


3
太棒了伙计!它在我的UWP上完美运行,并且我在WPF问题下找到了答案。赞一个。 - iam.Carrot
1
非常感谢你!它完美地解决了我的问题。在我的情况下,我有3个滚动视图器,实际上重新创建了一个具有冻结第一列和第一行的网格。只需扩展您的解决方案以包括3个滚动器,其中一个水平滚动,一个垂直滚动,第三个则两者兼备。如果您快速切换到使用鼠标滚轮滚动其他内容,可能会有轻微的延迟且有些混乱,但这很容易自行纠正。 - Graham
@Graham 是的,这就是这些动画的整个问题所在。你可以尝试调整超时时间,看看它是否会变得更好或更糟。 - René Sackers

1

好的,我基于https://www.codeproject.com/Articles/39244/Scroll-Synchronization做了一个实现,但我认为这个更简洁。

有一个同步滚动令牌,它持有要滚动的东西的引用。 然后有一个单独的附加属性。 我还没有弄清如何注销,因为引用仍然存在 - 所以我没有实现那部分。

嗯,就是这样:

public class SynchronisedScroll
{

    public static SynchronisedScrollToken GetToken(ScrollViewer obj)
    {
        return (SynchronisedScrollToken)obj.GetValue(TokenProperty);
    }
    public static void SetToken(ScrollViewer obj, SynchronisedScrollToken value)
    {
        obj.SetValue(TokenProperty, value);
    }
    public static readonly DependencyProperty TokenProperty =
        DependencyProperty.RegisterAttached("Token", typeof(SynchronisedScrollToken), typeof(SynchronisedScroll), new PropertyMetadata(TokenChanged));

    private static void TokenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var scroll = d as ScrollViewer;
        var oldToken = e.OldValue as SynchronisedScrollToken;
        var newToken = e.NewValue as SynchronisedScrollToken;

        if (scroll != null)
        {
            oldToken?.unregister(scroll);
            newToken?.register(scroll);
        }
    }
}

和另一个位

public class SynchronisedScrollToken
{
    List<ScrollViewer> registeredScrolls = new List<ScrollViewer>();

    internal void unregister(ScrollViewer scroll)
    {
        throw new NotImplementedException();
    }

    internal void register(ScrollViewer scroll)
    {
        scroll.ScrollChanged += ScrollChanged;
        registeredScrolls.Add(scroll);
    }

    private void ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        var sendingScroll = sender as ScrollViewer;
        foreach (var potentialScroll in registeredScrolls)
        {
            if (potentialScroll == sendingScroll)
                continue;

            if (potentialScroll.VerticalOffset != sendingScroll.VerticalOffset)
                potentialScroll.ScrollToVerticalOffset(sendingScroll.VerticalOffset);

            if (potentialScroll.HorizontalOffset != sendingScroll.HorizontalOffset)
                potentialScroll.ScrollToHorizontalOffset(sendingScroll.HorizontalOffset);
        }
    }
}

通过在某些资源中定义一个标记,以供需要进行滚动同步的所有对象访问使用。

<blah:SynchronisedScrollToken x:Key="scrollToken" />

然后通过以下方式在需要的地方使用它:
<ListView.Resources>
    <Style TargetType="ScrollViewer">
        <Setter Property="blah:SynchronisedScroll.Token"
                Value="{StaticResource scrollToken}" />
    </Style>
</ListView.Resources>

我只在垂直滚动时测试过它,对我有用。


1
如果有用的话,这里是一个行为(适用于UWP,但足以了解想法);使用行为有助于在MVVM设计中解耦视图和代码。
using Microsoft.Xaml.Interactivity;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

public class SynchronizeHorizontalOffsetBehavior : Behavior<ScrollViewer>
{
    public static ScrollViewer GetSource(DependencyObject obj)
    {
        return (ScrollViewer)obj.GetValue(SourceProperty);
    }

    public static void SetSource(DependencyObject obj, ScrollViewer value)
    {
        obj.SetValue(SourceProperty, value);
    }

    // Using a DependencyProperty as the backing store for Source.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SourceProperty =
        DependencyProperty.RegisterAttached("Source", typeof(object), typeof(SynchronizeHorizontalOffsetBehavior), new PropertyMetadata(null, SourceChangedCallBack));

    private static void SourceChangedCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        SynchronizeHorizontalOffsetBehavior synchronizeHorizontalOffsetBehavior = d as SynchronizeHorizontalOffsetBehavior;
        if (synchronizeHorizontalOffsetBehavior != null)
        {
            var oldSourceScrollViewer = e.OldValue as ScrollViewer;
            var newSourceScrollViewer = e.NewValue as ScrollViewer;
            if (oldSourceScrollViewer != null)
            {
                oldSourceScrollViewer.ViewChanged -= synchronizeHorizontalOffsetBehavior.SourceScrollViewer_ViewChanged;
            }
            if (newSourceScrollViewer != null)
            {
                newSourceScrollViewer.ViewChanged += synchronizeHorizontalOffsetBehavior.SourceScrollViewer_ViewChanged;
                synchronizeHorizontalOffsetBehavior.UpdateTargetViewAccordingToSource(newSourceScrollViewer);
            }
        }
    }

    private void SourceScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
    {
        ScrollViewer sourceScrollViewer = sender as ScrollViewer;
        this.UpdateTargetViewAccordingToSource(sourceScrollViewer);
    }

    private void UpdateTargetViewAccordingToSource(ScrollViewer sourceScrollViewer)
    {
        if (sourceScrollViewer != null)
        {
            if (this.AssociatedObject != null)
            {
                this.AssociatedObject.ChangeView(sourceScrollViewer.HorizontalOffset, null, null);
            }
        }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        var source = GetSource(this.AssociatedObject);
        this.UpdateTargetViewAccordingToSource(source);
    }
}

这是如何使用它的:

<ScrollViewer
      HorizontalScrollMode="Enabled"
      HorizontalScrollBarVisibility="Hidden"
      >
           <interactivity:Interaction.Behaviors>
              <behaviors:SynchronizeHorizontalOffsetBehavior Source="{Binding ElementName=ScrollViewer}" />
           </interactivity:Interaction.Behaviors>                                       
</ScrollViewer>
<ScrollViewer x:Name="ScrollViewer" />

0
在跟进Rene Sackers的C#代码清单后,我用VB.Net为UWP解决了同样的问题,并设置了超时以避免由于一个滚动视图对象触发事件而导致的错位效果,因为它的视图是由代码而不是用户交互改变的。我设置了500毫秒的超时时间,这对我的应用程序很有效。
注:svLvMain是一个滚动视图(对我来说是主窗口),svLVMainHeader是一个滚动视图(对我来说是放在主窗口上方的标题栏,我想要跟踪它和主窗口的变化)。缩放或滚动任一滚动视图都会使两个滚动视图保持同步。
Private Enum ScrollViewTrackingMasterSv
    Header = 1
    ListView = 2
    None = 0
End Enum

Private ScrollViewTrackingMaster As ScrollViewTrackingMasterSv
Private DispatchTimerForSvTracking As DispatcherTimer    

Private Sub DispatchTimerForSvTrackingSub(sender As Object, e As Object)
    ScrollViewTrackingMaster = ScrollViewTrackingMasterSv.None
    DispatchTimerForSvTracking.Stop()
End Sub

Private Sub svLvTracking(sender As Object, e As ScrollViewerViewChangedEventArgs, ByRef inMastScrollViewer As ScrollViewer)
    Dim tempHorOffset As Double
    Dim tempVerOffset As Double
    Dim tempZoomFactor As Single

    Dim tempSvMaster As New ScrollViewer
    Dim tempSvSlave As New ScrollViewer

    Select Case inMastScrollViewer.Name
        Case svLvMainHeader.Name

            Select Case ScrollViewTrackingMaster
                Case ScrollViewTrackingMasterSv.Header
                    tempSvMaster = svLvMainHeader
                    tempSvSlave = svLvMain

                    tempHorOffset = tempSvMaster.HorizontalOffset
                    tempVerOffset = tempSvMaster.VerticalOffset
                    tempZoomFactor = tempSvMaster.ZoomFactor

                    tempSvSlave.ChangeView(tempHorOffset, tempVerOffset, tempZoomFactor)

                    If DispatchTimerForSvTracking.IsEnabled Then
                        DispatchTimerForSvTracking.Stop()
                        DispatchTimerForSvTracking.Start()
                    End If

                Case ScrollViewTrackingMasterSv.ListView

                Case ScrollViewTrackingMasterSv.None
                    tempSvMaster = svLvMainHeader
                    tempSvSlave = svLvMain

                    ScrollViewTrackingMaster = ScrollViewTrackingMasterSv.Header
                    DispatchTimerForSvTracking = New DispatcherTimer()
                    AddHandler DispatchTimerForSvTracking.Tick, AddressOf DispatchTimerForSvTrackingSub
                    DispatchTimerForSvTracking.Interval = New TimeSpan(0, 0, 0, 0, 500)
                    DispatchTimerForSvTracking.Start()

                    tempHorOffset = tempSvMaster.HorizontalOffset
                    tempVerOffset = tempSvMaster.VerticalOffset
                    tempZoomFactor = tempSvMaster.ZoomFactor

                    tempSvSlave.ChangeView(tempHorOffset, tempVerOffset, tempZoomFactor)
            End Select


        Case svLvMain.Name

            Select Case ScrollViewTrackingMaster
                Case ScrollViewTrackingMasterSv.Header

                Case ScrollViewTrackingMasterSv.ListView

                    tempSvMaster = svLvMain
                    tempSvSlave = svLvMainHeader

                    tempHorOffset = tempSvMaster.HorizontalOffset
                    tempVerOffset = tempSvMaster.VerticalOffset
                    tempZoomFactor = tempSvMaster.ZoomFactor

                    tempSvSlave.ChangeView(tempHorOffset, tempVerOffset, tempZoomFactor)

                    If DispatchTimerForSvTracking.IsEnabled Then
                        DispatchTimerForSvTracking.Stop()
                        DispatchTimerForSvTracking.Start()
                    End If

                Case ScrollViewTrackingMasterSv.None
                    tempSvMaster = svLvMain
                    tempSvSlave = svLvMainHeader

                    ScrollViewTrackingMaster = ScrollViewTrackingMasterSv.ListView
                    DispatchTimerForSvTracking = New DispatcherTimer()
                    AddHandler DispatchTimerForSvTracking.Tick, AddressOf DispatchTimerForSvTrackingSub
                    DispatchTimerForSvTracking.Interval = New TimeSpan(0, 0, 0, 0, 500)
                    DispatchTimerForSvTracking.Start()

                    tempHorOffset = tempSvMaster.HorizontalOffset
                    tempVerOffset = tempSvMaster.VerticalOffset
                    tempZoomFactor = tempSvMaster.ZoomFactor

                    tempSvSlave.ChangeView(tempHorOffset, tempVerOffset, tempZoomFactor)
            End Select

        Case Else
            Exit Sub

    End Select


End Sub


Private Sub svLvMainHeader_ViewChanged(sender As Object, e As ScrollViewerViewChangedEventArgs) Handles svLvMainHeader.ViewChanged

    Call svLvTracking(sender, e, svLvMainHeader)

End Sub

Private Sub svLvMain_ViewChanged(sender As Object, e As ScrollViewerViewChangedEventArgs) Handles svLvMain.ViewChanged

    Call svLvTracking(sender, e, svLvMain)

End Sub

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接