Pues eso, que lo prometido es deuda y como te comenté en el post anterior ahora te voy a ensañar el código Xaml y de VB y C# para sincronizar dos textboxes de forma horizontal y la sincronización vertical la haremos en tres cajas de texto.
Nota:
Este post puede herir la sensibilidad de algunos, sobre todo de los serios y los que se toman la vida demasiado a pecho, aunque yo diría que con un egoísmo que no es práctico ni saludable
El que avisa…
En la figura 1 puedes ver una captura en tiempo de ejecución del programa. Como puedes apreciar, hay tres cajas de texto, dos de ellas más grandes con contenido y en el centro otra pero que nos muestra los números de líneas.
Las cajas de la izquierda y de la derecha las vamos a tener sincronizadas tanto vertical como horizontalmente, de forma que cuando te desplaces por el texto de cualquiera de ellas la otra se sincronice o muestre la misma línea y columna.
No voy a entrar en muchos detalles (salvo que lo considere estrictamente necesario), ya que básicamente lo que te he explicado en el post anterior (Scroll sincronizado en varios TextBox en WPF) te vale para este, así que… te mostraré el código completo tanto de la ventana (Window) el código Xaml; pero solo una de ellas, ya que tanto la de Visual Basic como la de C# son idénticas, salvo por el valor asignado en xmlns:local=»clr-namespace: y en el título de la ventana (asignado en Title).
El código XAML
<Window x:Class="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.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Wpf_scroll_completo_tres_textbox_vb" mc:Ignorable="d" Title="WPF Scroll sincronizado con tres TextBox (VB)" Height="450" Width="800" WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip" WindowStyle="ThreeDBorderWindow" Loaded="Window_Loaded"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto" MinWidth="36"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" MinHeight="24"/> <RowDefinition/> <RowDefinition/> <RowDefinition Height="Auto" MinHeight="24" MaxHeight="40"/> </Grid.RowDefinitions> <Label x:Name="lblInfo" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Background="{DynamicResource {x:Static SystemColors.InfoBrushKey}}" Margin="4" HorizontalContentAlignment="Center" Content="Dos TextBox sincronizados horizontal y verticalmente y otro sincronizado verticalmente"/> <!-- Si no queremos que se vean las barras de desplazamiento las ocultamos en el ScrollViewer (aunque estén ocultas siguen funcionando) --> <ScrollViewer x:Name="svIzquierda" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" HorizontalScrollBarVisibility="Visible" ScrollChanged="Sv_ScrollChanged"> <!-- Si el TextBox está contenido en un ScrollViewer no hace falta indicar en qué columna o fila del Grid está. --> <TextBox x:Name="txtIzquierda" Margin="0,2,2,0" Padding="4,0" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" TextChanged="TxtCodigo_TextChanged" Text="Aquí irá el texto a mostrar en el primer TextBox "/> </ScrollViewer> <!-- Si no queremos que se vean las barras de desplazamiento las ocultamos en el ScrollViewer (aunque estén ocultas siguen funcionando) --> <ScrollViewer x:Name="svFilas" Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" ScrollChanged="Sv_ScrollChanged"> <!-- Si el TextBox está contenido en un ScrollViewer no hace falta indicar en qué columna o fila del Grid está. --> <TextBox x:Name="txtFilas" TextWrapping="Wrap" Foreground="DarkCyan" AllowDrop="False" Focusable="False" IsTabStop="False"/> </ScrollViewer> <ScrollViewer x:Name="svDerecha" Grid.Column="2" Grid.Row="1" Grid.RowSpan="2" HorizontalScrollBarVisibility="Visible" ScrollChanged="Sv_ScrollChanged"> <TextBox x:Name="txtDerecha" Margin="0,2,2,0" Padding="4,0" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" TextChanged="TxtCodigo_TextChanged" Text="Aquí irá el texto a mostrar en el segundo TextBox" /> </ScrollViewer> <Label x:Name="lblInfo2" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Background="{DynamicResource {x:Static SystemColors.InfoBrushKey}}" Margin="4" HorizontalContentAlignment="Center" Content="Mueve el scroll vertical u horizontal o la rueda del ratón en el texto, el del centro no es visible, pero puedes desplazarte por el contenido."/> </Grid> </Window>
Todos los eventos que vamos a manejar en la aplicación están definidos en el propio código Xaml así será más fácil para todos, sobre todo para el Guille ya que así la asignación de los métodos de eventos es más fácil al cambiar entre Visual Basic y C#
Ya sabes que en Visual Basic podemos definir un método de evento simplemente añadiendo la cláusula Handles después del método, mientras que en C# hay que indicarlo expresamente.
Nota:
En una ocasión que hice un comentario como el anterior le añadí que así era mejor para los de Visual Basic que somos más torpes, y va uno y se enfadó y todo porque él no era torpe, en fin… hay que tomarse las cosas con una sonrisa, sobre todo si es una broma… pero ya sabes que en el mundillo este de la programación, si usas Visual Basic es que no sabes… y yo les digo a esos que así piensan: ¡yaumate!
(yaumate es eso… yaumate, no sé cómo traducirte esta expresión nerjeña, pero más o menos viene a decir ¡y un carajo! o ¡eso es lo que tú te crees!).
En mi caso, puede que yo sepa menos que muchos de los que usan C#, (también se mucho menos que muchos de los que usan VB) que ellos quieren usar el C#, pues muy bien, pero a mí me gusta programar con Visual Basic porque, al menos para mí, es más fácil que hacerlo con C#. Y eso que siempre, siempre, uso Option Strict On
Bueno dejemos las discrepancias ente los usuarios de VB y los de C# y veamos el código, sí, para C# y Visual Basic… y empezaré con el de C#… para dejar lo bueno para el postre (jijijiji que malillo soy)
// ---------------------------------------------------------------------------- // Scroll sincronizado con dos TextBox con WPF (06/Ene/19) // Sincronizados vertical y horizontalmente // // (c) Guillermo (elGuille) Som, 2019 // ---------------------------------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Wpf_scroll_completo_tres_textbox_cs { partial class MainWindow { private bool inicializando = true; private void Window_Loaded(object sender, RoutedEventArgs e) { txtIzquierda.Text = @"using System; using System.Diagnostics; static class Program { public static void Main(string[] args) { Console.WriteLine(""Hello World!""); Console.WriteLine(""La fecha y hora actual es: {0:dd/MM/yyyy HH:mm:ss:ffff}"", DateTime.Now); Console.WriteLine(); Console.WriteLine(""Usando Tuplas""); (Nombre As String, Fecha As DateTime) datos; // = (""elGuille"", DateTime.Now) datos = (""Guillermo"", DateTime.Now); Console.WriteLine(""{0} {1:dd/ MM / yyyy HH:mm : ss}"", datos.Nombre, datos.Fecha); Console.WriteLine(); // Probando con las dos formas var exe = ""dotnet""; var arg = ""--version""; Console.Write($""{exe} {arg} = ""); //Console.Write(""{0} {1} = "", exe, arg); // ejecutar el proceso para saber la versión de dotnet Core ejecutarProceso(exe, arg); Console.Write(""Versión del compilador: ""); var csc = @""C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\Roslyn\csc.exe""; ejecutarProceso(csc, ""-langversion:?""); float s = 173.7619f; double d = s; short s1 = System.Convert.ToInt16(Math.Truncate(s)); // Result: 173 int i2 = System.Convert.ToInt32(Math.Ceiling(d)); // Result: 174 int i3 = System.Convert.ToInt32(Math.Round(s)); // Result: 174 Console.WriteLine(""short s1 = System.Convert.ToInt16(Math.Truncate(s)); // Result: 173\r\n"" + ""int i2 = System.Convert.ToInt32(Math.Ceiling(d)); // Result: 174\r\n"" + ""int i3 = System.Convert.ToInt32(Math.Round(s)); // Result: 174""); Console.WriteLine(); Console.WriteLine($""s1= {s1}, i2 = {i2}, i3 = {i3}""); Console.WriteLine(""s1= {0}, i2 = {1}, i3 = {2}"", s1, i2, i3); Console.ReadKey(); } private static void ejecutarProceso(string exe, string arg, bool conKill = false) { Process p = new Process(); p.StartInfo.FileName = exe; p.StartInfo.Arguments = arg; // Indicamos que queremos redirigir la salida p.StartInfo.RedirectStandardOutput = true; // Para redirigir la salida, UseShellExecute debe ser falso p.StartInfo.UseShellExecute = false; p.StartInfo.CreateNoWindow = true; try { // Iniciamos el proceso p.Start(); // Esperar a que el proceso finalice // // Esperamos 2 segundos para que le de tiempo a ejecutarse p.WaitForExit(2000); if (conKill) p.Kill(); // Mostrar la salida en la consola Console.WriteLine(p.StandardOutput.ReadToEnd()); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } "; txtDerecha.Text = @"Option Strict On Option Infer On Imports Microsoft.VisualBasic Imports System Imports System.Diagnostics Module Program Sub Main(args As String()) Console.WriteLine(""Hello World!"") Console.WriteLine(""La fecha y hora actual es: {0:dd/MM/yyyy HH:mm:ss:ffff}"", Date.Now) Console.WriteLine() Console.WriteLine(""Usando Tuplas"") Dim datos As (Nombre As String, Fecha As Date) ' = (""elGuille"", Date.Now) datos = (""Guillermo"", Date.Now) Console.WriteLine(""{0} {1:dd/MM/yyyy HH:mm:ss}"", datos.Nombre, datos.Fecha) Console.WriteLine() ' Probando con las dos formas Dim exe = ""dotnet"" Dim arg = ""--version"" Console.Write($""{exe} {arg} = "") 'Console.Write(""{0} {1} = "", exe, arg) ' ejecutar el proceso para saber la versión de dotnet Core ejecutarProceso(exe, arg) Console.Write(""Versión del compilador: "") Dim vbc = ""C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\Roslyn\vbc.exe"" ejecutarProceso(vbc, ""-langversion:?"") Dim s As Single = 173.7619 Dim d As Double = s Dim i1 As Integer = CInt(Fix(s)) ' Result: 173 Dim b1 As Byte = CByte(Int(d)) ' Result: 173 Dim s1 As Short = CShort(Math.Truncate(s)) ' Result: 173 Dim i2 As Integer = CInt(Math.Ceiling(d)) ' Result: 174 Dim i3 As Integer = CInt(Math.Round(s)) ' Result: 174 Console.WriteLine(""Dim i1 As Integer = CInt(Fix(s)) ' Result: 173"" & vbCrLf & ""Dim b1 As Byte = CByte(Int(d)) ' Result: 173"" & vbCrLf & ""Dim s1 As Short = CShort(Math.Truncate(s)) ' Result: 173"" & vbCrLf & ""Dim i2 As Integer = CInt(Math.Ceiling(d)) ' Result: 174"" & vbCrLf & ""Dim i3 As Integer = CInt(Math.Round(s)) ' Result: 174"") Console.WriteLine() Console.WriteLine(""i1= {0}, b1 = {1}"", i1, b1) Console.WriteLine($""s1= {s1}, i2 = {i2}, i3 = {i3}"") Console.WriteLine(""s1= {0}, i2 = {1}, i3 = {2}"", s1, i2, i3) Console.ReadKey() End Sub Private Sub ejecutarProceso(exe As String, arg As String, Optional conKill As Boolean = False) Dim p As New Process p.StartInfo.FileName = exe p.StartInfo.Arguments = arg ' Indicamos que queremos redirigir la salida p.StartInfo.RedirectStandardOutput = True ' Para redirigir la salida, UseShellExecute debe ser falso p.StartInfo.UseShellExecute = False p.StartInfo.CreateNoWindow = True Try ' Iniciamos el proceso p.Start() ' Esperar a que el proceso finalice ' ' Esperamos 2 segundos para que le de tiempo a ejecutarse p.WaitForExit(2000) If conKill Then p.Kill() End If ' Mostrar la salida en la consola Console.WriteLine(p.StandardOutput.ReadToEnd()) Catch ex As Exception Console.WriteLine(ex.Message) End Try End Sub End Module "; inicializando = false; // que se muestren las líneas TxtCodigo_TextChanged(null, null); } /// <summary> /// Sincronizar el scroll vertical de los TextBox /// de izquierda, derecha y las filas. /// Sincronizar el scroll horizontal de los dos TextBox /// de izquierda y derecha. /// </summary> private void Sv_ScrollChanged(object sender, ScrollChangedEventArgs e) { if (inicializando) return; inicializando = true; var sv = sender as ScrollViewer; if (sv == svIzquierda) { svDerecha.ScrollToVerticalOffset(e.VerticalOffset); svDerecha.ScrollToHorizontalOffset(e.HorizontalOffset); svFilas.ScrollToVerticalOffset(e.VerticalOffset); } else if (sv == svDerecha) { svIzquierda.ScrollToVerticalOffset(e.VerticalOffset); svIzquierda.ScrollToHorizontalOffset(e.HorizontalOffset); svFilas.ScrollToVerticalOffset(e.VerticalOffset); } else if (sv == svFilas) { svIzquierda.ScrollToVerticalOffset(e.VerticalOffset); svDerecha.ScrollToVerticalOffset(e.VerticalOffset); } inicializando = false; } /// <summary> /// Al cambiar el texto del código actualizar el número de líneas. /// Se tiene en cuenta cuál de los dos TextBox tiene más líneas. /// </summary> private void TxtCodigo_TextChanged(object sender, TextChangedEventArgs e) { if (inicializando) return; // Contar las líneas var linIzq = txtIzquierda.LineCount; var linDer = txtDerecha.LineCount; // Comprobar cuál de los dos textBoxes tiene más líneas var lin = linIzq > linDer ? linIzq : linDer; txtFilas.Text = ""; for (var i = 1; i <= lin; i++) // Indentar el texto a la derecha txtFilas.Text += i.ToString("0").PadLeft(4) + "\r"; } } }
Y ahora el de Visual Basic
'------------------------------------------------------------------------------ ' Scroll sincronizado con dos TextBox con WPF (06/Ene/19) ' Sincronizados vertical y horizontalmente ' ' (c) Guillermo (elGuille) Som, 2019 '------------------------------------------------------------------------------ Option Strict On Option Infer On Imports Microsoft.VisualBasic Imports System Imports System.Windows Imports System.Windows.Controls Class MainWindow Private inicializando As Boolean = True Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs) txtIzquierda.Text = "using System; using System.Diagnostics; static class Program { public static void Main(string[] args) { Console.WriteLine(""Hello World!""); Console.WriteLine(""La fecha y hora actual es: {0:dd/MM/yyyy HH:mm:ss:ffff}"", DateTime.Now); Console.WriteLine(); Console.WriteLine(""Usando Tuplas""); (Nombre As String, Fecha As DateTime) datos; // = (""elGuille"", DateTime.Now) datos = (""Guillermo"", DateTime.Now); Console.WriteLine(""{0} {1:dd/ MM / yyyy HH:mm : ss}"", datos.Nombre, datos.Fecha); Console.WriteLine(); // Probando con las dos formas var exe = ""dotnet""; var arg = ""--version""; Console.Write($""{exe} {arg} = ""); //Console.Write(""{0} {1} = "", exe, arg); // ejecutar el proceso para saber la versión de dotnet Core ejecutarProceso(exe, arg); Console.Write(""Versión del compilador: ""); var csc = @""C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\Roslyn\csc.exe""; ejecutarProceso(csc, ""-langversion:?""); float s = 173.7619f; double d = s; short s1 = System.Convert.ToInt16(Math.Truncate(s)); // Result: 173 int i2 = System.Convert.ToInt32(Math.Ceiling(d)); // Result: 174 int i3 = System.Convert.ToInt32(Math.Round(s)); // Result: 174 Console.WriteLine(""short s1 = System.Convert.ToInt16(Math.Truncate(s)); // Result: 173\r\n"" + ""int i2 = System.Convert.ToInt32(Math.Ceiling(d)); // Result: 174\r\n"" + ""int i3 = System.Convert.ToInt32(Math.Round(s)); // Result: 174""); Console.WriteLine(); Console.WriteLine($""s1= {s1}, i2 = {i2}, i3 = {i3}""); Console.WriteLine(""s1= {0}, i2 = {1}, i3 = {2}"", s1, i2, i3); Console.ReadKey(); } private static void ejecutarProceso(string exe, string arg, bool conKill = false) { Process p = new Process(); p.StartInfo.FileName = exe; p.StartInfo.Arguments = arg; // Indicamos que queremos redirigir la salida p.StartInfo.RedirectStandardOutput = true; // Para redirigir la salida, UseShellExecute debe ser falso p.StartInfo.UseShellExecute = false; p.StartInfo.CreateNoWindow = true; try { // Iniciamos el proceso p.Start(); // Esperar a que el proceso finalice // // Esperamos 2 segundos para que le de tiempo a ejecutarse p.WaitForExit(2000); if (conKill) p.Kill(); // Mostrar la salida en la consola Console.WriteLine(p.StandardOutput.ReadToEnd()); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } " txtDerecha.Text = "Option Strict On Option Infer On Imports Microsoft.VisualBasic Imports System Imports System.Diagnostics Module Program Sub Main(args As String()) Console.WriteLine(""Hello World!"") Console.WriteLine(""La fecha y hora actual es: {0:dd/MM/yyyy HH:mm:ss:ffff}"", Date.Now) Console.WriteLine() Console.WriteLine(""Usando Tuplas"") Dim datos As (Nombre As String, Fecha As Date) ' = (""elGuille"", Date.Now) datos = (""Guillermo"", Date.Now) Console.WriteLine(""{0} {1:dd/MM/yyyy HH:mm:ss}"", datos.Nombre, datos.Fecha) Console.WriteLine() ' Probando con las dos formas Dim exe = ""dotnet"" Dim arg = ""--version"" Console.Write($""{exe} {arg} = "") 'Console.Write(""{0} {1} = "", exe, arg) ' ejecutar el proceso para saber la versión de dotnet Core ejecutarProceso(exe, arg) Console.Write(""Versión del compilador: "") Dim vbc = ""C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\Roslyn\vbc.exe"" ejecutarProceso(vbc, ""-langversion:?"") Dim s As Single = 173.7619 Dim d As Double = s Dim i1 As Integer = CInt(Fix(s)) ' Result: 173 Dim b1 As Byte = CByte(Int(d)) ' Result: 173 Dim s1 As Short = CShort(Math.Truncate(s)) ' Result: 173 Dim i2 As Integer = CInt(Math.Ceiling(d)) ' Result: 174 Dim i3 As Integer = CInt(Math.Round(s)) ' Result: 174 Console.WriteLine(""Dim i1 As Integer = CInt(Fix(s)) ' Result: 173"" & vbCrLf & ""Dim b1 As Byte = CByte(Int(d)) ' Result: 173"" & vbCrLf & ""Dim s1 As Short = CShort(Math.Truncate(s)) ' Result: 173"" & vbCrLf & ""Dim i2 As Integer = CInt(Math.Ceiling(d)) ' Result: 174"" & vbCrLf & ""Dim i3 As Integer = CInt(Math.Round(s)) ' Result: 174"") Console.WriteLine() Console.WriteLine(""i1= {0}, b1 = {1}"", i1, b1) Console.WriteLine($""s1= {s1}, i2 = {i2}, i3 = {i3}"") Console.WriteLine(""s1= {0}, i2 = {1}, i3 = {2}"", s1, i2, i3) Console.ReadKey() End Sub Private Sub ejecutarProceso(exe As String, arg As String, Optional conKill As Boolean = False) Dim p As New Process p.StartInfo.FileName = exe p.StartInfo.Arguments = arg ' Indicamos que queremos redirigir la salida p.StartInfo.RedirectStandardOutput = True ' Para redirigir la salida, UseShellExecute debe ser falso p.StartInfo.UseShellExecute = False p.StartInfo.CreateNoWindow = True Try ' Iniciamos el proceso p.Start() ' Esperar a que el proceso finalice ' ' Esperamos 2 segundos para que le de tiempo a ejecutarse p.WaitForExit(2000) If conKill Then p.Kill() End If ' Mostrar la salida en la consola Console.WriteLine(p.StandardOutput.ReadToEnd()) Catch ex As Exception Console.WriteLine(ex.Message) End Try End Sub End Module " inicializando = False ' que se muestren las líneas TxtCodigo_TextChanged(Nothing, Nothing) End Sub ''' <summary> ''' Sincronizar el scroll vertical de los TextBox ''' de izquierda, derecha y las filas. ''' Sincronizar el scroll horizontal de los dos TextBox ''' de izquierda y derecha. ''' </summary> Private Sub Sv_ScrollChanged(sender As Object, e As ScrollChangedEventArgs) If inicializando Then Return inicializando = True Dim sv = TryCast(sender, ScrollViewer) If sv Is svIzquierda Then svDerecha.ScrollToVerticalOffset(e.VerticalOffset) svDerecha.ScrollToHorizontalOffset(e.HorizontalOffset) svFilas.ScrollToVerticalOffset(e.VerticalOffset) ElseIf sv Is svDerecha Then svIzquierda.ScrollToVerticalOffset(e.VerticalOffset) svIzquierda.ScrollToHorizontalOffset(e.HorizontalOffset) svFilas.ScrollToVerticalOffset(e.VerticalOffset) ElseIf sv Is svFilas Then svIzquierda.ScrollToVerticalOffset(e.VerticalOffset) svDerecha.ScrollToVerticalOffset(e.VerticalOffset) End If inicializando = False End Sub ''' <summary> ''' Al cambiar el texto del código actualizar el número de líneas. ''' Se tiene en cuenta cuál de los dos TextBox tiene más líneas. ''' </summary> Private Sub TxtCodigo_TextChanged(sender As Object, e As TextChangedEventArgs) If inicializando Then Return ' Contar las líneas Dim linIzq = txtIzquierda.LineCount Dim linDer = txtDerecha.LineCount ' Comprobar cuál de los dos textBoxes tiene más líneas Dim lin = If(linIzq > linDer, linIzq, linDer) txtFilas.Text = "" For i = 1 To lin ' Indentar el texto a la derecha txtFilas.Text &= i.ToString("0").PadLeft(4) & vbCr Next End Sub End Class
El tocho de código ese que está en el evento Window_Loaded es solo para que haya bastante contenido en las dos cajas de texto, precisamente un código de C# y otro de VB.
Las diferencias en el código del post anterior y este es que aquí manejamos tres controles desplazables y por tanto tenemos que tener en cuenta los tres ScrollViewer cuando se produce el evento ScrollChanged en alguno de ellos.
Las dos cajas de textos grandes, la de la izquierda (txtIzquierda) y la de la derecha (txtDerecha) en la figura 1, están sincronizadas tanto verticalmente como horizontalmente por eso si el control ScrollViewer que produce el evento ScrollChanged es cualquiera de los dos que contienen esas cajas de texto los sincronizamos horizontalmente asignando el valor ScrollToHorizontalOffset del otro ScrollViewer para que coincida con el que ha producido el evento. Como la caja de textos con los números solo queremos sincronizarla verticalmente, solo asignamos el valor a ScrollToVerticalOffset. Por supuesto ese valor también se lo asignamos a la otra caja de textos.
Y en caso de que el control de desplazamiento que produce el evento de cambio de desplazamiento sea svFilas (el de los números de líneas) no es necesario sincronizar la posición horizontal.
En el evento TextChanged tenemos en cuenta cuál de los dos TextBox tienen más líneas, para asignar ese valor a los números a mostrar.
El número de líneas lo obtenemos de la propiedad LineCount del TextBox.
Y ya está… en otra ocasión te pondré un ejemplo parecido con dos RichTextBox y además te explicaré cómo obtener el texto que contenga y cómo asignar un nuevo texto (en formato RTF evidentemente).
Pero eso será otro día.
Nos vemos.
Guillermo
El ZIP contiene la solución para Visual Studio con 4 ejemplos, dos del post anterior y los dos (VB y C#) de este.
El zip: Wpf_scroll_textbox.zip (50.3 KB)
MD5 Checksum: E226C01209749FB18ABD9F17C5E3FD80
Nota:
Este es un nuevo zip con el código actualizado a fecha del 07/Ene/2018 02.04 hora de España.