Desplazamiento sincronizado de dos ScrollViewers cada vez que se desplaza uno en wpf

He repasado el hilo:

unir dos VerticalScrollBars uno a otro

casi ha ayudado a lograr el objective, pero todavía falta algo. Es que mover las barras de desplazamiento izquierda-derecha o arriba-abajo da el comportamiento esperado de desplazamiento en mis dos visualizadores de desplazamiento pero cuando intentamos desplazarnos usando los botones de flecha / haciendo clic en los extremos de estas barras de desplazamiento en los visualizadores de desplazamiento solo se desplaza un visualizador de desplazamiento que no está el comportamiento esperado.

Entonces, ¿qué más necesitamos agregar / editar para resolver esto?

Una forma de hacerlo es usar el evento ScrollChanged para actualizar el otro 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); } } 

La pregunta es por WPF, pero en caso de que alguien que desarrolla UWP tropiece con esto, tuve que tomar un enfoque ligeramente diferente.
En UWP, cuando configura el desplazamiento de desplazamiento del otro visor de desplazamiento (usando ScrollViewer.ChangeView ), también desencadena el evento ViewChanged en el otro visor de desplazamiento, básicamente creando un bucle, causando que sea muy intermitente y no funcione correctamente.

Resolví esto usando un poco de tiempo de espera en el manejo del evento, si el objeto que se está desplazando no es igual al último objeto que manejó el evento.

XAML:

  ...   ...  

Código detrás:

 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); } } 

Tenga en cuenta que el tiempo de espera es de 500 ms. Esto puede parecer un poco largo, pero como las aplicaciones UWP tienen una animación (o, suavizando, realmente) en su desplazamiento (cuando se usa la rueda de desplazamiento en un mouse), hace que el evento se active unas pocas veces en unos pocos cientos de milisegundos . Este tiempo de espera parece funcionar perfectamente.

Si puede ser útil, aquí hay un comportamiento (para UWP, pero es suficiente para obtener la idea); el uso de un comportamiento ayuda a desacoplar la vista y el código en un diseño MVVM.

 using Microsoft.Xaml.Interactivity; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; public class SynchronizeHorizontalOffsetBehavior : Behavior { 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); } } 

He aquí cómo usarlo:

       

En el seguimiento de la lista de códigos de Rene Sackers en C # para UWP, aquí es cómo resolví este mismo problema en VB.Net para UWP con un tiempo de espera para evitar el efecto asombroso debido a que un Objeto Scroll Viewer activó el evento porque su vista fue cambiada por el código y no por la interacción del usuario. Puse un período de espera de 500 milisegundos que funciona bien para mi aplicación.

Notas: svLvMain es un visualizador de desplazamiento (para mí es la ventana principal) svLVMainHeader es un visualizador de desplazamiento (para mí es el encabezado que va por encima de la ventana principal y es lo que quiero rastrear junto con la ventana principal y viceversa). Al hacer zoom o desplazarse, scrollviewer mantendrá sincronizados a ambos visualizadores de desplazamiento.

 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