Синхронизированная прокрутка двух ScrollViewer всякий раз, когда любой прокручивается в wpf

Я прошел через нить:

привязка двух VerticalScrollBars один к другому

это почти помогло достичь цели, но чего-то не хватает. Дело в том, что перемещение полос прокрутки влево-вправо или вверх-вниз дает ожидаемое поведение прокрутки в обоих моих средствах прокрутки, но когда мы пытаемся прокрутить, используя / нажимая кнопки со стрелками на концах этих полос прокрутки в средствах прокрутки, прокручивается только один вид прокрутки, который не ожидаемое поведение.

Так что еще нам нужно добавить / отредактировать, чтобы решить это?

4 ответа

Решение

Одним из способов сделать это является использование 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);
        }
    }

Вопрос к 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 имеют анимацию (или, действительно, замедление) при прокрутке (при использовании колеса прокрутки на мыши), это вызывает событие, которое запускается несколько раз в течение нескольких сотен миллисекунд., Этот тайм-аут, кажется, работает отлично.

Ну, я сделал реализацию на основе 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>

Я тестировал его только при вертикальной прокрутке, и у меня он работает.

Если это может быть полезно, вот поведение (для 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" />

В продолжение листинга кода Рене Сакерса в C# для UWP, вот как я решил эту же проблему в VB.Net для UWP с таймаутом, чтобы избежать ошеломляющего эффекта из-за того, что один объект просмотра прокрутки запускает событие, потому что его представление было изменено код, а не путем взаимодействия с пользователем. Я установил период ожидания 500 миллисекунд, который хорошо работает для моего приложения.

Примечания: svLvMain - это средство просмотра прокрутки (для меня это главное окно) svLVMainHeader - средство просмотра прокрутки (для меня это заголовок, который находится над главным окном и который я хочу отслеживать вместе с главным окном и наоборот). Масштабирование или прокрутка любого scrollviewer будет синхронизировать оба scrollviewer.

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
Другие вопросы по тегам