每当在 wpf 中滚动任何一个 ScrollViewer 时,两个 ScrollViewer 就会同步滚动

问题描述 投票:0回答:6

我已经浏览了该线程:

将两个 VerticalScrollBar 相互绑定

已经差不多达到目标了,但还是少了点什么。左右或上下移动滚动条可以在我的两个滚动查看器中提供预期的滚动行为,但是当我们尝试使用/单击滚动查看器中这些滚动条末端的箭头按钮进行滚动时,只有一个滚动查看器滚动,而这不是预期的行为。

那么我们还需要添加/编辑什么来解决这个问题?

c# .net wpf scrollviewer
6个回答
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);
        }
    }

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 应用程序在滚动时有动画(或者实际上是缓动)(当使用鼠标上的滚轮时),它会导致事件在几百毫秒内触发几次。这个超时似乎非常有效。


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" />

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>

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


0
投票

在跟进 Rene Sackers 在 C# for UWP 中列出的代码时,以下是我如何在 VB.Net for UWP 中解决同样的问题,并设置超时,以避免由于一个 Scroll Viewer 对象触发事件(因为它的视图已更改)而产生令人震惊的效果通过代码而不是用户交互。我设置了 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

0
投票

按照上一个答案,DataGrid 怎么样? 因为dataGrid中没有.ScrollToHorizontalOffset。

© www.soinside.com 2019 - 2024. All rights reserved.