我已经完成了线程:
它几乎有助于实现目标,但仍然缺少一些东西。左右或上下移动滚动条会在我的两个滚动查看器中提供预期的滚动行为,但是当我们尝试使用/单击滚动查看器中这些滚动条末端的箭头按钮进行滚动时,只有一个滚动查看器被滚动,这不是预期的行为。
那么我们还需要添加/编辑什么来解决这个问题?
我已经完成了线程:
它几乎有助于实现目标,但仍然缺少一些东西。左右或上下移动滚动条会在我的两个滚动查看器中提供预期的滚动行为,但是当我们尝试使用/单击滚动查看器中这些滚动条末端的箭头按钮进行滚动时,只有一个滚动查看器被滚动,这不是预期的行为。
那么我们还需要添加/编辑什么来解决这个问题?
一种方法是使用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" />
好吧,我根据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 的 C# 中的 Rene Sackers 代码列表时,以下是我如何在 VB.Net 中为 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