The nicest way I could think of doing this is by making a custom control to encapsulate the behaviour like the following:
public class RulerScrollViewer : ScrollViewer
{
static RulerScrollViewer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(RulerScrollViewer), new FrameworkPropertyMetadata(typeof(RulerScrollViewer)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.ScrollChanged -= RulerScrollViewer_ScrollChanged;
this.ScrollChanged += RulerScrollViewer_ScrollChanged;
}
void RulerScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var h = this.Template.FindName("PART_HorizontalRulerScrollViewer", this) as ScrollViewer;
if (h != null)
h.ScrollToHorizontalOffset(this.HorizontalOffset);
var v = this.Template.FindName("PART_VerticalRulerScrollViewer", this) as ScrollViewer;
if (v != null)
v.ScrollToVerticalOffset(this.VerticalOffset);
}
}
public class HorizontalRuler : Control
{
protected override void OnRender(DrawingContext drawingContext)
{
for (int i = 0; i < this.ActualWidth / 100; i++)
{
var ft = new FormattedText(i.ToString(), CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, new Typeface("Tahoma"), 8, Brushes.Black);
drawingContext.DrawText(ft, new Point(i * 100, 0));
}
}
}
public class VerticallRuler : Control
{
protected override void OnRender(DrawingContext drawingContext)
{
for (int i = 0; i < this.ActualHeight / 100; i++)
{
var ft = new FormattedText(i.ToString(), CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, new Typeface("Tahoma"), 8, Brushes.Black);
drawingContext.DrawText(ft, new Point(0, i*100));
}
}
}
With something like the following in the generic.xaml for it:
<ControlTemplate x:Key="RulerScrollViewer_Template" TargetType="local:RulerScrollViewer">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer x:Name="PART_HorizontalRulerScrollViewer" Grid.Row="0" Grid.Column="1" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
<local:HorizontalRuler Width="{Binding ElementName=content,Path=ExtentWidth}" Height="20" />
</ScrollViewer>
<ScrollViewer x:Name="PART_VerticalRulerScrollViewer" Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
<local:VerticallRuler Height="{Binding ElementName=content,Path=ExtentHeight}" Width="20" />
</ScrollViewer>
<ScrollContentPresenter Name="content" Grid.Row="1" Grid.Column="1" />
<ScrollBar x:Name="PART_VerticalScrollBar"
Grid.Row="1" Grid.Column="2"
Value="{TemplateBinding VerticalOffset}"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
<ScrollBar x:Name="PART_HorizontalScrollBar"
Orientation="Horizontal"
Grid.Row="2" Grid.Column="1"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type local:RulerScrollViewer}">
<Setter Property="Template" Value="{StaticResource RulerScrollViewer_Template}" />
</Style>
Edit:
My reasoning for making it a custom control is because I wanted to gaurentee that the width of the horizontal ruler, and the height of the vertical ruler, were the same size size as the visible content. This was so I didn't get the weird behaviour of the rulers moving at slightly different rates vs the content due to the visible content area being slightly smaller from the scrollbars being inside the ScrollViewer.