Подтвердить что ты не робот

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

Я прошел через поток:

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

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

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

4b9b3361

Ответ 1

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

Ответ 2

Вопрос заключается в 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, но этого достаточно, чтобы получить идею); использование поведения помогает разделить вид и код в дизайне 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" />

Ответ 4

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

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

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

Ответ 5

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

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