Синхронизированная двух ScrollViewers когда-либо прокручивается в WPF
Я прошел через нить:
привязка двух VerticalScrollBars один к другому
Это почти помогло достичь цели, но все же чего-то не хватает. Это то, что перемещение полос прокрутки влево-вправо или вверх-вниз дает ожидаемое поведение прокрутки в обоих моих scrollviewers, но когда мы пытаемся прокрутить с помощью кнопок со стрелками / щелчком на концах этих полос прокрутки в scrollviewers прокручивается только один scrollviewer, который не является ожидаемое поведение.
Итак, что еще нам нужно добавить / отредактировать, чтобы решить эту проблему?
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 есть анимация (или, действительно, облегчение) в их прокрутке (при использовании колеса прокрутки на мыши), это заставляет событие запускаться несколько раз в течение нескольких сотен миллисекунд. Этот тайм-аут, кажется, работает отлично.
Если это может быть полезно, вот поведение (для 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" />
при отслеживании списка кода Rene Sackers в C# для UWP, вот как я решил эту же проблему в VB.Net для UWP с таймаутом, чтобы избежать эффекта ошеломления из-за одного объекта просмотра прокрутки, запускающего событие, поскольку его представление было изменено кодом, а не взаимодействием пользователя. Я поставил 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