Synchronizing Two Rich Text Box Scroll bars in WPF
Categories:
Synchronizing Two RichTextBox Scroll Bars 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.
Synchronizing the scroll bars of two or more controls is a common requirement in WPF applications, especially when displaying related content side-by-side. For RichTextBox
controls, this can be particularly useful for comparing documents, displaying code with line numbers, or showing original and translated texts. This article will guide you through the process of achieving seamless vertical scroll synchronization between two RichTextBox
instances.
Understanding the Challenge
The RichTextBox
control in WPF does not directly expose its internal ScrollViewer
component, which is responsible for handling scrolling. This makes direct binding or manipulation of scroll properties challenging. To overcome this, we need to access the ScrollViewer
within the RichTextBox
's visual tree and then hook into its ScrollChanged
event. When one RichTextBox
scrolls, we'll programmatically update the scroll position of the other.

Synchronization mechanism for two RichTextBox controls
Accessing the ScrollViewer
The first step is to find the ScrollViewer
instance within the RichTextBox
. Since ScrollViewer
is a templated part of RichTextBox
, we can use the VisualTreeHelper
to traverse the visual tree and locate it. A helper method can simplify this process.
using System.Windows.Controls;
using System.Windows.Media;
public static class RichTextBoxHelper
{
public static ScrollViewer GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer scrollViewer)
{
return scrollViewer;
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result != null)
{
return result;
}
}
return null;
}
}
Helper method to find the ScrollViewer within a DependencyObject
GetScrollViewer
helper method is a generic way to find a ScrollViewer
within any control's visual tree. It uses recursion to search through all child elements until a ScrollViewer
is found.Implementing Scroll Synchronization
Once we can access the ScrollViewer
for both RichTextBox
controls, we can subscribe to their ScrollChanged
events. In the event handler, we will check which RichTextBox
initiated the scroll and then update the vertical scroll offset of the other RichTextBox
accordingly. To prevent an infinite loop of events, we'll use a flag to temporarily disable event handling.
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)
{
_scrollViewer1 = RichTextBoxHelper.GetScrollViewer(richTextBox1);
_scrollViewer2 = RichTextBoxHelper.GetScrollViewer(richTextBox2);
if (_scrollViewer1 != null) _scrollViewer1.ScrollChanged += ScrollViewer1_ScrollChanged;
if (_scrollViewer2 != null) _scrollViewer2.ScrollChanged += ScrollViewer2_ScrollChanged;
// Example 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} in RichTextBox 1.")));
}
richTextBox2.Document.Blocks.Add(new Paragraph(new Run("This is content for RichTextBox 2.\n")));
for (int i = 0; i < 50; i++)
{
richTextBox2.Document.Blocks.Add(new Paragraph(new Run($"Line {i + 1} in RichTextBox 2.")));
}
}
private void ScrollViewer1_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_isScrolling) return;
_isScrolling = true;
if (_scrollViewer2 != null) _scrollViewer2.ScrollToVerticalOffset(e.VerticalOffset);
_isScrolling = false;
}
private void ScrollViewer2_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_isScrolling) return;
_isScrolling = true;
if (_scrollViewer1 != null) _scrollViewer1.ScrollToVerticalOffset(e.VerticalOffset);
_isScrolling = false;
}
}
C# code-behind for synchronizing RichTextBox scrollbars
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Synchronized RichTextBoxes" 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 layout for two RichTextBox controls
_isScrolling
flag is crucial to prevent a StackOverflowException
. Without it, scrolling one RichTextBox
would trigger the other to scroll, which would then trigger the first, creating an endless loop of ScrollChanged
events.1. Set up your WPF project
Create a new WPF Application project in Visual Studio.
2. Define XAML layout
Add two RichTextBox
controls to your MainWindow.xaml
with distinct x:Name
attributes and set VerticalScrollBarVisibility="Visible"
.
3. Create RichTextBoxHelper
class
Add a new static class named RichTextBoxHelper.cs
to your project and paste the GetScrollViewer
method into it.
4. Implement synchronization logic
In your MainWindow.xaml.cs
code-behind, add the _scrollViewer1
, _scrollViewer2
, and _isScrolling
fields. Implement the MainWindow_Loaded
event to find the ScrollViewer
instances and subscribe to their ScrollChanged
events. Finally, implement the ScrollViewer1_ScrollChanged
and ScrollViewer2_ScrollChanged
methods to handle the synchronization logic.
5. Run the application
Execute your application. You should now be able to scroll either RichTextBox
, and the other will follow its vertical scroll position.