やりたいこと
複数の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を内蔵しているので、思わぬ影響が出ることがある。