65.9K
CodeProject 正在变化。 阅读更多。
Home

滚动同步

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (58投票s)

2009年8月24日

CPOL

4分钟阅读

viewsIcon

176711

downloadIcon

5726

同步多个控件的滚动位置。

引言

想象一下,您有两个包含大量项目的ListBox。当用户滚动其中一个 ListBox 时,另一个也应该被更新。本文的目标是创建一个简单的附加属性,允许我们对可滚动控件进行分组。在下面的示例中,您将看到两个 ScrollViewer,它们的滚动位置是同步的,因为它们都附加到同一个 ScrollGroup,“Group1”。

<ScrollViewer 
    Name="ScrollViewer1" 
    scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollViewer>
<ScrollViewer 
    Name="ScrollViewer2" 
    scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollViewer>

由于大多数可滚动控件在其模板中使用 ScrollViewer 来实现滚动,因此只要它们在其 ControlTemplate 中包含 ScrollViewer,此方法也适用于 ListBoxTreeView 等其他控件。

您可以在 http://www.software-architects.com/devblog/2009/10/13/Scroll-Synchronization-in-WPF-and-Silverlight 找到我的同步 ListBox 的 Silverlight 版本。

在接下来的文章中,我将展示如何在 WPF 中构建 ScrollSyncronizer 类,以同步各种可滚动控件的滚动位置。在源代码下载中,您将找到 WPF 和 Silverlight 的工作解决方案。

构建 ScrollSynchronizer

我们的 ScrollSynchronizer 对象在 UI 中没有表示。它只负责提供附加属性 ScrollGroup。因此,我选择 DependencyObject 作为基类。首先,我向类添加了附加依赖属性 ScrollGroup 及其相应的 GetScrollGroupSetScrollGroup 方法。

public class ScrollSynchronizer : DependencyObject
{
    public static readonly DependencyProperty ScrollGroupProperty =
	    DependencyProperty.RegisterAttached(
	    "ScrollGroup", 
	    typeof(string), 
	    typeof(ScrollSynchronizer), 
	    new PropertyMetadata(new PropertyChangedCallback(
	    OnScrollGroupChanged)));

    public static void SetScrollGroup(DependencyObject obj, string scrollGroup)
    {
        obj.SetValue(ScrollGroupProperty, scrollGroup);
    }

    public static string GetScrollGroup(DependencyObject obj)
    {
        return (string)obj.GetValue(ScrollGroupProperty);
    }
    ...
}

在新属性的属性元数据中,有一个回调,每次 ScrollViewer 使用附加属性时都会调用它。所以,这里是我们提供逻辑以将 ScrollViewer 与所有其他附加的 ScrollViewer 同步的地方。但在那之前,我们需要一些私有字段来存储所有附加的 ScrollViewer 以及它们相应的水平和垂直偏移量。这些字典中的字符串部分是由 ScrollGroup 属性设置的组名。

private static Dictionary<ScrollViewer, string> scrollViewers = 
    new Dictionary<ScrollViewer, string>();

private static Dictionary<string, double> horizontalScrollOffsets = 
    new Dictionary<string, double>();

private static Dictionary<string, double> verticalScrollOffsets = 
    new Dictionary<string, double>();

现在,我们可以实现 ScrollGroup 属性更改的回调。基本上,代码很简单。当通过设置附加属性添加新的 ScrollViewer 时,我们会检查是否已在 horizontalScrollOffsetverticalScrollOffset 字段中找到该组的滚动位置。如果是,我们调整新 ScrollViewer 的滚动位置,使其与组匹配。否则,我们在 horizontalScrollOffsetverticalScrollOffset 中添加一个条目,其中包含新 ScrollViewer 的当前滚动位置。最后,我们将新 ScrollViewer 添加到 scrollViewers 字典中,并附带其组名,然后添加一个事件处理程序来处理 ScrollChanged 事件,以便在滚动位置发生更改时,我们可以调整组中的所有其他 ScrollViewer

如果删除了附加属性,我们会从列表中删除 ScrollViewer。在这种情况下,我们不会删除 horizontalScrollOffsetverticalScrollOffset 中的条目,即使它是组中的最后一个 ScrollViewer,因为当稍后将另一个 ScrollViewer 添加到该组时,我们仍然知道该组的最后一个滚动位置。

private static void OnScrollGroupChanged(DependencyObject d, 
                    DependencyPropertyChangedEventArgs e)
{
    var scrollViewer = d as ScrollViewer;
    if (scrollViewer != null)
    {
        if (!string.IsNullOrEmpty((string)e.OldValue))
        {
            // Remove scrollviewer
            if (scrollViewers.ContainsKey(scrollViewer))
            {
                scrollViewer.ScrollChanged -= 
                  new ScrollChangedEventHandler(ScrollViewer_ScrollChanged);
                scrollViewers.Remove(scrollViewer);
            }
        }

        if (!string.IsNullOrEmpty((string)e.NewValue))
        {
            // If group already exists, set scrollposition of 
            // new scrollviewer to the scrollposition of the group
            if (horizontalScrollOffsets.Keys.Contains((string)e.NewValue))
            {
                scrollViewer.ScrollToHorizontalOffset(
                              horizontalScrollOffsets[(string)e.NewValue]);
            }
            else
            {
                horizontalScrollOffsets.Add((string)e.NewValue, 
                                        scrollViewer.HorizontalOffset);
            }

            if (verticalScrollOffsets.Keys.Contains((string)e.NewValue))
            {
                scrollViewer.ScrollToVerticalOffset(verticalScrollOffsets[(string)e.NewValue]);
            }
            else
            {
                verticalScrollOffsets.Add((string)e.NewValue, scrollViewer.VerticalOffset);
            }

            // Add scrollviewer
            scrollViewers.Add(scrollViewer, (string)e.NewValue);
            scrollViewer.ScrollChanged += 
                new ScrollChangedEventHandler(ScrollViewer_ScrollChanged);
        }
    }
}

现在,我们的最后一个任务是实现 ScrollChanged 事件的处理程序。如果水平或垂直滚动位置发生变化,我们会将 verticalScrollOffsetshorizontalScrollOffsets 字典更新到最新位置。然后,我们需要找到与更改的 ScrollViewer 属于同一组的所有 ScrollViewer,并更新它们的滚动位置。

private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
    if (e.VerticalChange != 0 || e.HorizontalChange != 0)
    {
        var changedScrollViewer = sender as ScrollViewer;
        Scroll(changedScrollViewer);
    }
}

private static void Scroll(ScrollViewer changedScrollViewer)
{
    var group = scrollViewers[changedScrollViewer];
    verticalScrollOffsets[group] = changedScrollViewer.VerticalOffset;
    horizontalScrollOffsets[group] = changedScrollViewer.HorizontalOffset;

    foreach (var scrollViewer in scrollViewers.Where((s) => s.Value == 
                                      group && s.Key != changedScrollViewer))
    {
        if (scrollViewer.Key.VerticalOffset != changedScrollViewer.VerticalOffset)
        {
            scrollViewer.Key.ScrollToVerticalOffset(changedScrollViewer.VerticalOffset);
        }

        if (scrollViewer.Key.HorizontalOffset != changedScrollViewer.HorizontalOffset)
        {
            scrollViewer.Key.ScrollToHorizontalOffset(changedScrollViewer.HorizontalOffset);
        }
    }
}

测试 ScrollSynchronizer

为了测试新的附加属性,我们构建了一个包含两个 ScrollViewer 的简单 UI。对于这两个 ScrollViewer,我们将 ScrollGroup 属性设置为“Group1”。

<Window 
  xmlns:scroll="clr-namespace:SoftwareArchitects.Windows.Controls;
  assembly=SoftwareArchitects.Windows.Controls.ScrollSynchronizer"
  ...>
    <Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <ScrollViewer Grid.Column="0" Name="ScrollViewer1" 
                     Margin="0,0,5,0" 
                     scroll:ScrollSynchronizer.ScrollGroup="Group1">
            <StackPanel Name="Panel1" />
        </ScrollViewer>

        <ScrollViewer Grid.Column="1" Name="ScrollViewer2" 
                     Margin="5,0,0,0" 
                     scroll:ScrollSynchronizer.ScrollGroup="Group1">
            <StackPanel Name="Panel2" />
        </ScrollViewer>
    </Grid>
</Window>

在代码隐藏文件中,我们在两个面板中添加了一些 TextBlock,以便 ScrollBar 可以显示出来。

public Window1()
{
    InitializeComponent();
    // Fill listboxes
    for (var i = 0; i < 100; i++)
    {
        this.Panel1.Children.Add(new TextBlock() 
                { Text = string.Format("This is item {0}", i) });
        this.Panel2.Children.Add(new TextBlock() 
                { Text = string.Format("This is item {0}", i) });
    }
}

完成!我们有两个同步的 ScrollViewer

同步 ListBox

那么,我们如何才能同步其他控件呢?让我们用两个 ListBox 替换 ScrollViewer。不幸的是,我们不能将附加属性 ScrollGroup 设置给 ListBox。在 OnScrollGroupChanged 回调中,我们假设我们总是会得到一个 ScrollViewer。因此,我们可以增强 ScrollSynchronizer 来接受其他类型的控件,或者我们可以简单地为 ListBox 中的 ScrollViewer 添加一个样式,该样式会设置 ScrollGroup 属性。在这种情况下,我们的 ScrollSynchronizer 不需要任何更改。

<ListBox Grid.Column="0" Name="ListBox1" Margin="0,0,5,0">
    <ListBox.Resources>
        <Style TargetType="ScrollViewer">
            <Setter Property="scroll:ScrollSynchronizer.ScrollGroup" Value="Group1" />
        </Style>
    </ListBox.Resources>
</ListBox>
 

<ListBox Grid.Column="1" Name="ListBox2" Margin="5,0,0,0">
    <ListBox.Resources>
        <Style TargetType="ScrollViewer">
            <Setter Property="scroll:ScrollSynchronizer.ScrollGroup" Value="Group1" />
        </Style>
    </ListBox.Resources>
</ListBox>

更好的方法是在 Grid 资源中设置样式,这样它就可以自动应用于网格中的所有 ScrollViewer

<Grid.Resources>
    <Style TargetType="ScrollViewer">
        <Setter Property="scroll:ScrollSynchronizer.ScrollGroup" Value="Group1" />
    </Style>
</Grid.Resources>

<ListBox Grid.Column="0" Name="ListBox1" Margin="0,0,5,0" />

<ListBox Grid.Column="1" Name="ListBox2" Margin="5,0,0,0" />

Silverlight 支持

基本上,这个解决方案也适用于 Silverlight。具体来说,存在一些差异,例如 Silverlight 中的 ScrollViewer 不提供 ScrollChanged 事件。但是,您可以通过使用底层 ScrollBarScrollValueChanged 事件来绕过这个问题。另一个问题是,即使使用 ImplicitStyleManagerStyle 对于 ListBox 示例中的 ScrollViewer 也未应用。因此,我最终为 Silverlight 在代码中设置了附加属性。在源代码下载中,您将找到 WPF 和 Silverlight 的工作解决方案。在 http://www.software-architects.at/TechnicalArticles/ScrollSync/tabid/101/language/en-US/Default.aspx,您可以查看 Silverlight 同步 Listbox 的在线演示。

滚动同步 - CodeProject - 代码之家
© . All rights reserved.