Synchronizing Two Rich Text Box Scroll bars in WPF
Categories:
Synchronizing Two RichTextBox Scrollbars in WPF

Learn how to effectively synchronize the vertical scroll positions of two RichTextBox controls in WPF, enabling a unified viewing experience for related content.
In WPF applications, there are scenarios where you might need to display related content in two separate RichTextBox controls and ensure their vertical scroll positions remain synchronized. This is particularly useful for comparing documents, displaying code with line numbers, or showing original text alongside a translated version. Unlike some other controls, RichTextBox does not expose a direct ScrollChanged event or a simple VerticalOffset property for easy synchronization. This article will guide you through the process of achieving this synchronization by leveraging the visual tree and event handlers.
Understanding the Challenge
The primary challenge with RichTextBox is its complex internal structure. It's not a simple control; it's a ContentControl that hosts a FlowDocument and internally uses a ScrollViewer to manage its scrollable content. However, this internal ScrollViewer is not directly exposed as a public property, making it difficult to access its scroll events or manipulate its scroll position directly. We need a way to 'reach into' the RichTextBox's visual tree to find and interact with its internal ScrollViewer.

Conceptual structure of a RichTextBox and its internal ScrollViewer
Accessing the Internal ScrollViewer
To synchronize the scrollbars, we first need to get a reference to the ScrollViewer instance within each RichTextBox. This can be achieved by traversing the visual tree. The VisualTreeHelper class in WPF provides static methods to navigate the visual tree of UI elements. We'll create a helper method to find a child of a specific type within the visual tree.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
public static class VisualTreeHelpers
{
public static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child != null && child is T)
{
return (T)child;
}
else
{
T childOfChild = FindVisualChild<T>(child);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return null;
}
}
Helper method to find a visual child of a specific type
Implementing Scroll Synchronization
Once we can access the ScrollViewer for each RichTextBox, the synchronization logic becomes straightforward. We'll attach to the ScrollChanged event of one ScrollViewer and, within its event handler, update the scroll position of the other ScrollViewer. To prevent an infinite loop (where ScrollViewer1 scrolls ScrollViewer2, which then scrolls ScrollViewer1 again), we'll use a flag to indicate when a scroll operation is initiated programmatically.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
public partial class MainWindow : Window
{
private ScrollViewer _scrollViewer1;
private ScrollViewer _scrollViewer2;
private bool _isScrolling;
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
// Find the internal ScrollViewer for each RichTextBox
_scrollViewer1 = VisualTreeHelpers.FindVisualChild<ScrollViewer>(richTextBox1);
_scrollViewer2 = VisualTreeHelpers.FindVisualChild<ScrollViewer>(richTextBox2);
if (_scrollViewer1 != null && _scrollViewer2 != null)
{
_scrollViewer1.ScrollChanged += ScrollViewer_ScrollChanged;
_scrollViewer2.ScrollChanged += ScrollViewer_ScrollChanged;
// Optional: Populate RichTextBoxes with some content
richTextBox1.Document.Blocks.Add(new Paragraph(new Run("This is content for RichTextBox 1.\n")));
for (int i = 0; i < 50; i++)
{
richTextBox1.Document.Blocks.Add(new Paragraph(new Run($"Line {i + 1} of RichTextBox 1 content.")));
richTextBox2.Document.Blocks.Add(new Paragraph(new Run($"Corresponding line {i + 1} for RichTextBox 2.")));
}
}
}
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_isScrolling) return; // Prevent re-entry
_isScrolling = true;
ScrollViewer currentScrollViewer = sender as ScrollViewer;
if (currentScrollViewer == _scrollViewer1)
{
// Synchronize _scrollViewer2 to _scrollViewer1's position
_scrollViewer2.ScrollToVerticalOffset(currentScrollViewer.VerticalOffset);
}
else if (currentScrollViewer == _scrollViewer2)
{
// Synchronize _scrollViewer1 to _scrollViewer2's position
_scrollViewer1.ScrollToVerticalOffset(currentScrollViewer.VerticalOffset);
}
_isScrolling = false;
}
}
C# code for synchronizing RichTextBox scrollbars
InitializeComponent() in your MainWindow constructor if you are using XAML. The MainWindow_Loaded event ensures that the RichTextBox controls are fully rendered and their internal ScrollViewer instances are available before attempting to access them.XAML Setup
For this solution to work, you'll need two RichTextBox controls in your XAML, each with a unique x:Name attribute. A simple Grid or StackPanel can be used to arrange them side-by-side.
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.microsoft.com/winfx/2006/xaml/presentation/markup-compatibility/2006"
mc:Ignorable="d"
Title="RichTextBox Scroll Sync" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<RichTextBox x:Name="richTextBox1" Grid.Column="0" Margin="5" VerticalScrollBarVisibility="Visible" />
<RichTextBox x:Name="richTextBox2" Grid.Column="1" Margin="5" VerticalScrollBarVisibility="Visible" />
</Grid>
</Window>
XAML for two RichTextBox controls
FindVisualChild helper method performs a depth-first search. While generally effective, for very complex visual trees, it might have a slight performance impact. For RichTextBox, the ScrollViewer is usually found relatively quickly. Ensure VerticalScrollBarVisibility="Visible" is set on your RichTextBox controls if you want to explicitly show the scrollbars.1. Create a new WPF project
Start by creating a new WPF Application project in Visual Studio.
2. Add XAML for RichTextBoxes
Modify your MainWindow.xaml to include two RichTextBox controls as shown in the XAML example.
3. Implement VisualTreeHelpers
Create a new static class (e.g., VisualTreeHelpers.cs) and add the FindVisualChild<T> method.
4. Add Synchronization Logic
In your MainWindow.xaml.cs code-behind, add the fields, constructor, MainWindow_Loaded event handler, and ScrollViewer_ScrollChanged method as provided in the C# example.
5. Run and Test
Execute your application. You should now be able to scroll either RichTextBox, and the other will follow its vertical scroll position.