Edited at

ScrollViewerのスクロール位置を同期する


やりたいこと

複数のScrollViewerのスクロール位置を同期させる。ただし、同期する相手をグループに分けられるようにする。


実装

スクロール位置を同期させるビヘイビアを作成する。


ビヘイビア

using System.Collections.Generic;

using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;

namespace Sample {
public class ScrollSynchronizer {
//ScrollViewerとグループ名の対応リスト
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>();

//グループ名の添付プロパティ
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)
=> (string)obj.GetValue(ScrollGroupProperty);

private static void OnScrollGroupChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var scrollViewer = d as ScrollViewer;

if (scrollViewer == null)
return;

var oldGroup = (string)e.OldValue ?? "";
var newGroup = (string)e.NewValue ?? "";

if (oldGroup != "") {
if (_scrollViewers.ContainsKey(scrollViewer)) {
//登録解除
scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
_scrollViewers.Remove(scrollViewer);
}
}

if (newGroup != "") {
if (_scrollViewers.ContainsValue(newGroup)) {
//既存のグループ名の場合は、現在のスクロール位置を反映する
scrollViewer.ScrollToHorizontalOffset(_horizontalScrollOffsets[newGroup]);
scrollViewer.ScrollToVerticalOffset(_verticalScrollOffsets[newGroup]);
} else {
//新しいグループ名の場合は、スクロール位置を記録する
_horizontalScrollOffsets[newGroup] = scrollViewer.HorizontalOffset;
_verticalScrollOffsets[newGroup] = scrollViewer.VerticalOffset;
}

//ScrollViewerを登録
_scrollViewers.Add(scrollViewer, newGroup);
scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
}
}

private static void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) {
var changedScrollViewer = sender as ScrollViewer;
var group = _scrollViewers[changedScrollViewer];
var scrollViewers = _scrollViewers.Where(s => s.Value == group && s.Key != changedScrollViewer);

//垂直方向
if (e.VerticalChange != 0) {
_verticalScrollOffsets[group] = changedScrollViewer.VerticalOffset;

//同じグループのScrollViewerにスクロール位置を反映
scrollViewers
.Where(s => s.Key.VerticalOffset != _verticalScrollOffsets[group])
.Select(x => x.Key)
.ForEach(scrollViewer => scrollViewer.ScrollToVerticalOffset(_verticalScrollOffsets[group]));
}

//水平方向
if (e.HorizontalChange != 0) {
_horizontalScrollOffsets[group] = changedScrollViewer.HorizontalOffset;

//同じグループのScrollViewerにスクロール位置を反映
scrollViewers
.Where(s => s.Key.HorizontalOffset != _horizontalScrollOffsets[group])
.Select(x => x.Key)
.ForEach(scrollViewer => scrollViewer.ScrollToHorizontalOffset(_horizontalScrollOffsets[group]));
}
}
}
}



使い方


XAML

<Window x:Class="Sample.MainView"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Sample"
Title="MainView" Height="300" Width="300">
<DockPanel Orientation="Vertical">
<!--同期させたいScrollViewerに同じグループ名を指定する-->
<ScrollViewer local:ScrollSynchronizer.ScrollGroup="Group1">
<StackPanel>
<Button Content="Button1" />
<!--中略-->
<Button Content="Button15" />
</StackPanel>
</ScrollViewer>
<ScrollViewer local:ScrollSynchronizer.ScrollGroup="Group1">
<StackPanel>
<Button Content="Button1" />
<!--中略-->
<Button Content="Button15" />
</StackPanel>
</ScrollViewer>
</DockPanel>
</Window>


注意事項

スタイルで使用する場合は、ScrollViewerが入れ子になっていないか注意する。入れ子にしていないつもりでも、DataGridやComboBoxはScrollViewerを内蔵しているので、思わぬ影響が出ることがある。