Introduction
I came across Scroll Synchronization which I utilized in my own project. But it needed to have its capabilities expanded and a couple of flaws fixed...
Background
Please refer to the original article this is in response to, to understand the basics which this article builds upon.
The original only covers the use of basic synchronized ScrollViewer
s and doesn't handle situations like programmatic
generation of visual elements etc... You end up with scroll bars not being set correctly etc... or not updating properly and it also only handles
ScrollViewer
s... Whatever happened to using ScrollBar
s as well???
So I thought everyone might get some use out of this expanded ScrollSynchronizer
... which addresses these issues.
Using the code
Once again it is the same simple class structure as specified in the original article. It uses the same simple attached property...
<ScrollViewer
Name="ScrollViewer1"
scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollViewer>
<ScrollViewer
Name="ScrollViewer2"
scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollViewer>
But it now supports attaching the property to ScrollBar
s as well... at the same time!!!
<ScrollBar
Name="ScrollBar1"
scroll:ScrollSynchronizer.ScrollGroup="Group1">
...
</ScrollBar>
I now have two ScrollViewer
s and one ScrollBar
, all linked together via "Group1".
How neat is that!!! And everything behaves the way you think it would. If it doesn't go ahead and hack the relevant code...
You can also mix and match the orientations or the ScrollBar
s and the visibility of the ScrollViewer
scrollbars.
The code makes sure only the relevant oriented scrollbars are updated etc...
Demo Project
I've supplied a simple demo project so you can see the linked scrollviewers and scrollbars all linked together and working. So download and have a play.
Note I've purposely made the scrollviewers and the scrollbars all different sizes so you can see that it all still works together as you would expect.
Code
So here is the code:
Please note I like to use capitalized keywords like Double
instead of double
- it looks nicer in the editor...
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace Temp
{
public class ScrollSynchronizer : DependencyObject
{
public static DependencyProperty ScrollGroupProperty;
static Dictionary<Object, String> m_Scrollers;
static Dictionary<String, List<Object>> m_GroupScrollers;
static Dictionary<String, Double> m_HorizontalScrollPositions;
static Dictionary<String, Double> m_HorizontalScrollLengths;
static Dictionary<String, Double> m_VerticalScrollPositions;
static Dictionary<String, Double> m_VerticalScrollLengths;
public String ScrollGroup
{
get { return (String)GetValue(ScrollGroupProperty); }
set { SetValue(ScrollGroupProperty, value); }
}
static ScrollSynchronizer()
{
ScrollGroupProperty = DependencyProperty.RegisterAttached("ScrollGroup", typeof(String),
typeof(ScrollSynchronizer),
new PropertyMetadata(new PropertyChangedCallback(OnScrollGroupChanged)));
m_Scrollers = new Dictionary<Object, String>();
m_GroupScrollers = new Dictionary<String, List<Object>>();
m_HorizontalScrollPositions = new Dictionary<String, Double>();
m_HorizontalScrollLengths = new Dictionary<String, Double>();
m_VerticalScrollPositions = new Dictionary<String, Double>();
m_VerticalScrollLengths = new Dictionary<String, Double>();
}
public static void SetScrollGroup(DependencyObject obj, String nScrollGroup)
{
obj.SetValue(ScrollGroupProperty, nScrollGroup);
}
public static String GetScrollGroup(DependencyObject obj)
{
return (String)obj.GetValue(ScrollGroupProperty);
}
static void OnScrollGroupChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
ScrollViewer sv = obj as ScrollViewer;
ScrollBar sb = obj as ScrollBar;
if ((sv == null) && (sb == null))
return;
String ov = (String)e.OldValue;
String nv = (String)e.NewValue;
if (!String.IsNullOrEmpty(ov))
{
if ((sv != null) && (m_Scrollers.ContainsKey(sv)))
{
sv.ScrollChanged -= new ScrollChangedEventHandler(ScrollViewer_ScrollChanged);
m_Scrollers.Remove(sv);
m_GroupScrollers[ov].Remove(sv);
}
if ((sb != null) && (m_Scrollers.ContainsKey(sb)))
{
sb.IsEnabled = false;
sb.Scroll -= new ScrollEventHandler(ScrollBar_Scroll);
m_Scrollers.Remove(sb);
m_GroupScrollers[ov].Remove(sb);
}
if (m_GroupScrollers[ov].Count == 0)
{
m_GroupScrollers.Remove(ov);
m_HorizontalScrollPositions.Remove(ov);
m_HorizontalScrollLengths.Remove(ov);
m_VerticalScrollPositions.Remove(ov);
m_VerticalScrollLengths.Remove(ov);
}
}
if (!String.IsNullOrEmpty(nv))
{
if (!m_GroupScrollers.ContainsKey(nv))
m_GroupScrollers.Add(nv, new List<Object>());
if (sv != null)
{
if (sv.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled)
{
if (m_HorizontalScrollPositions.ContainsKey(nv))
SetScrollViewerHorizontalPosition(sv, m_HorizontalScrollPositions[nv]);
else
{
m_HorizontalScrollPositions.Add(nv, GetScrollViewerHorizontalPosition(sv));
m_HorizontalScrollLengths.Add(nv, GetScrollViewerHorizontalLength(sv));
}
}
if (sv.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled)
{
if (m_VerticalScrollPositions.ContainsKey(nv))
SetScrollViewerVerticalPosition(sv, m_VerticalScrollPositions[nv]);
else
{
m_VerticalScrollPositions.Add(nv, GetScrollViewerVerticalPosition(sv));
m_VerticalScrollLengths.Add(nv, GetScrollViewerVerticalLength(sv));
}
}
m_Scrollers.Add(sv, nv);
m_GroupScrollers[nv].Add(sv);
sv.ScrollChanged += new ScrollChangedEventHandler(ScrollViewer_ScrollChanged);
}
if (sb != null)
{
sb.IsEnabled = true;
if (sb.Orientation == Orientation.Horizontal)
{
if (m_HorizontalScrollPositions.ContainsKey(nv))
{
SetScrollBarPosition(sb, m_HorizontalScrollPositions[nv]);
SetScrollBarLength(sb, m_HorizontalScrollLengths[nv]);
}
else
{
m_HorizontalScrollPositions.Add(nv, GetScrollBarPosition(sb));
m_HorizontalScrollLengths.Add(nv, GetScrollBarLength(sb));
}
}
else
{
if (m_VerticalScrollPositions.ContainsKey(nv))
{
SetScrollBarPosition(sb, m_VerticalScrollPositions[nv]);
SetScrollBarLength(sb, m_VerticalScrollLengths[nv]);
}
else
{
m_VerticalScrollPositions.Add(nv, GetScrollBarPosition(sb));
m_VerticalScrollLengths.Add(nv, GetScrollBarLength(sb));
}
}
m_Scrollers.Add(sb, nv);
m_GroupScrollers[nv].Add(sb);
sb.Scroll += new ScrollEventHandler(ScrollBar_Scroll);
}
}
}
static void ScrollViewer_ScrollChanged(Object sender, ScrollChangedEventArgs e)
{
Scroll(sender as ScrollViewer, (e.HorizontalChange != 0), (e.VerticalChange != 0));
}
static void ScrollBar_Scroll(Object sender, ScrollEventArgs e)
{
Scroll(sender as ScrollBar, false, false);
}
static void Scroll(Object nChangeScroller, Boolean nHorzChange, Boolean nVertChange)
{
String group = m_Scrollers[nChangeScroller];
ScrollViewer svc = nChangeScroller as ScrollViewer;
ScrollBar sbc = nChangeScroller as ScrollBar;
if (svc != null)
{
if (svc.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled)
{
m_HorizontalScrollPositions[group] = GetScrollViewerHorizontalPosition(svc);
m_HorizontalScrollLengths[group] = GetScrollViewerHorizontalLength(svc);
}
if (svc.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled)
{
m_VerticalScrollPositions[group] = GetScrollViewerVerticalPosition(svc);
m_VerticalScrollLengths[group] = GetScrollViewerVerticalLength(svc);
}
}
if (sbc != null)
{
if (sbc.Orientation == Orientation.Horizontal)
{
m_HorizontalScrollPositions[group] = GetScrollBarPosition(sbc);
m_HorizontalScrollLengths[group] = GetScrollBarLength(sbc);
}
else
{
m_VerticalScrollPositions[group] = GetScrollBarPosition(sbc);
m_VerticalScrollLengths[group] = GetScrollBarLength(sbc);
}
}
foreach (Object obj in m_GroupScrollers[group])
{
ScrollViewer sv = obj as ScrollViewer;
ScrollBar sb = obj as ScrollBar;
if ((sv == nChangeScroller) || (sb == nChangeScroller))
continue;
if (sv != null)
{
if (svc != null)
{
if ((sv.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled) && nHorzChange
&& (svc.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled))
SetScrollViewerHorizontalPosition(sv, GetScrollViewerHorizontalPosition(svc));
if ((sv.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled) && nVertChange
&& (svc.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled))
SetScrollViewerVerticalPosition(sv, GetScrollViewerVerticalPosition(svc));
}
if (sbc != null)
{
if ((sv.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled)
&& (sbc.Orientation == Orientation.Horizontal))
SetScrollViewerHorizontalPosition(sv, GetScrollBarPosition(sbc));
if ((sv.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled)
&& (sbc.Orientation == Orientation.Vertical))
SetScrollViewerVerticalPosition(sv, GetScrollBarPosition(sbc));
}
}
if (sb != null)
{
if (svc != null)
{
if ((sb.Orientation == Orientation.Horizontal)
&& (svc.HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled))
{
SetScrollBarPosition(sb, GetScrollViewerHorizontalPosition(svc));
SetScrollBarLength(sb, GetScrollViewerHorizontalLength(svc));
}
if ((sb.Orientation == Orientation.Vertical)
&& (svc.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled))
{
SetScrollBarPosition(sb, GetScrollViewerVerticalPosition(svc));
SetScrollBarLength(sb, GetScrollViewerVerticalLength(svc));
}
}
if (sbc != null)
{
if (sb.Orientation == sbc.Orientation)
{
SetScrollBarPosition(sb, GetScrollBarPosition(sbc));
SetScrollBarLength(sb, GetScrollBarLength(sbc));
}
}
}
}
}
static Double GetScrollViewerHorizontalPosition(ScrollViewer nScrollViewer)
{
if (nScrollViewer.ViewportWidth >= nScrollViewer.ExtentWidth)
return 0;
return nScrollViewer.HorizontalOffset / nScrollViewer.ScrollableWidth;
}
static Double GetScrollViewerHorizontalLength(ScrollViewer nScrollViewer)
{
if (nScrollViewer.ViewportWidth >= nScrollViewer.ExtentWidth)
return 1;
if (nScrollViewer.ExtentWidth <= 0)
return 0;
return nScrollViewer.ViewportWidth / nScrollViewer.ExtentWidth;
}
static Double GetScrollViewerVerticalPosition(ScrollViewer nScrollViewer)
{
if (nScrollViewer.ViewportHeight >= nScrollViewer.ExtentHeight)
return 0;
return nScrollViewer.VerticalOffset / nScrollViewer.ScrollableHeight;
}
static Double GetScrollViewerVerticalLength(ScrollViewer nScrollViewer)
{
if (nScrollViewer.ViewportHeight >= nScrollViewer.ExtentHeight)
return 1;
if (nScrollViewer.ExtentHeight <= 0)
return 0;
return nScrollViewer.ViewportHeight / nScrollViewer.ExtentHeight;
}
static Double GetScrollBarPosition(ScrollBar nScrollBar)
{
Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;
return (nScrollBar.Value - nScrollBar.Minimum) / tracklen;
}
static Double GetScrollBarLength(ScrollBar nScrollBar)
{
Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;
return nScrollBar.ViewportSize / (tracklen + nScrollBar.ViewportSize);
}
static void SetScrollViewerHorizontalPosition(ScrollViewer nScrollViewer, Double nPosition)
{
nScrollViewer.ScrollToHorizontalOffset(nPosition * nScrollViewer.ScrollableWidth);
}
static void SetScrollViewerVerticalPosition(ScrollViewer nScrollViewer, Double nPosition)
{
nScrollViewer.ScrollToVerticalOffset(nPosition * nScrollViewer.ScrollableHeight);
}
static void SetScrollBarPosition(ScrollBar nScrollBar, Double nPosition)
{
Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;
nScrollBar.Value = nPosition * tracklen + nScrollBar.Minimum;
}
static void SetScrollBarLength(ScrollBar nScrollBar, Double nLength)
{
Double tracklen = nScrollBar.Maximum - nScrollBar.Minimum;
if (nLength < 1)
{
nScrollBar.ViewportSize = nLength * tracklen / (1 - nLength);
nScrollBar.LargeChange = nScrollBar.ViewportSize;
nScrollBar.IsEnabled = true;
}
else
{
nScrollBar.ViewportSize = Double.MaxValue;
nScrollBar.IsEnabled = false;
}
}
}
}