Pues eso… que estoy preparando el artículo para usar dos RichTextBox de WPF sincronizados y esta es una de las cosas que allí (cuando lo publique) te explicaré. Hay muchas cosas que te tengo que explicar, así que… hoy empiezo por la más simple o que menos texto necesita
Esto es sencillo, pero… lo necesité para mostrar el número de palabras del documento activo (en el ejemplo de los RichTextBox sincronizados voy a usar 2 RichTextBox y un TextBox, los tres sincronizados -si así lo decides- ) y no lo investigué… preferí buscar una solución que alguien haya publicado ya… es que esto de las expresiones regulares, más que regulares son malas jajajajaja.
En serio, encontré un sitio web, en realidad un foro de Microsoft en el que alguien respondía con el código que te voy a mostrar, eso sí, el código original está en C# y para la aplicación base (ya sabes que empiezo escribiendo los ejemplos o mis aplicaciones con Visual Basic para .NET).
Una vez que se ve el código, uno dice… ¡pos qué fácil era! pero como todas estas cosas, hay que hacerlo primero
Este es el código de una función que recibe una cadena de texto y devuelve el número de palabras que hay.
La versión de Visual Basic .NET
'''<summary>
''' Contar las palabras de una cadena de texto
'''''' Adaptado usando una cadena en vez del Text del RichTextBox
''' (sería del RichTextBox para WinForms)
''' El código lo he adaptado de:
''' https://social.msdn.microsoft.com/Forums/en-US/
''' 81e438ed-9d35-47d7-a800-1fabab0f3d52/
''' c-how-to-add-a-word-counter-to-a-richtextbox
''' ?forum=csharplanguage
'''</summary>
PrivateFunction TextWordCount(texto AsString) AsInteger
Dim wordColl As MatchCollection = Regex.Matches(texto, "[\W]+")
Return wordColl.Count
EndFunction
La versión de C#:
///<summary>
/// Contar las palabras de una cadena de texto
////// Adaptado usando una cadena en vez del Text del RichTextBox
/// (sería del RichTextBox para WinForms)
/// El código lo he adaptado de:
/// https://social.msdn.microsoft.com/Forums/en-US/
/// 81e438ed-9d35-47d7-a800-1fabab0f3d52/
/// c-how-to-add-a-word-counter-to-a-richtextbox
/// ?forum=csharplanguage
///</summary>
privateint TextWordCount(string texto)
{
MatchCollection wordColl = Regex.Matches(texto, @"[\W]+");
return wordColl.Count;
}
Aquí tienes un ejemplo de uso en una aplicación de consola de .NET Core para Visual Basic y la salida del código.
Imports System
Imports System.Text.RegularExpressions
ModuleProgramSub Main(args AsString())
Dim s = "Hello World de las aplicaciones de consola en .net core!"
Console.WriteLine(s)
Dim w = TextWordCount(s)
Console.WriteLine($"la cadena anterior tiene {w} palabras")
Console.ReadKey()
EndSub'''<summary>''' Contar las palabras de una cadena de texto'''''' Adaptado usando una cadena en vez del Text del RichTextBox''' (sería del RichTextBox para WinForms)''' El código lo he adaptado de:''' https://social.msdn.microsoft.com/Forums/en-US/''' 81e438ed-9d35-47d7-a800-1fabab0f3d52/''' c-how-to-add-a-word-counter-to-a-richtextbox''' ?forum=csharplanguage'''</summary>PrivateFunction TextWordCount(texto AsString) AsIntegerDim wordColl As MatchCollection = Regex.Matches(texto, "[\W]+")
Return wordColl.Count
EndFunctionEndModule
Y esta es la salida por la consola de ese código.
Y esto es todo.
¡Mañana más!
Nos vemos. Guillermo
P.S. Ese código funciona también en .NET Core, la captura es de una aplicación de consola de .NET Core
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.
Figura 1. La aplicación con el contenido de los textboxes sincronizados.
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
{
partialclassMainWindow
{
privatebool inicializando = true;
privatevoid 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>privatevoid 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);
}
elseif (sv == svDerecha)
{
svIzquierda.ScrollToVerticalOffset(e.VerticalOffset);
svIzquierda.ScrollToHorizontalOffset(e.HorizontalOffset);
svFilas.ScrollToVerticalOffset(e.VerticalOffset);
}
elseif (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>privatevoid TxtCodigo_TextChanged(object sender, TextChangedEventArgs e)
{
if (inicializando)
return;
// Contar las líneasvar linIzq = txtIzquierda.LineCount;
var linDer = txtDerecha.LineCount;
// Comprobar cuál de los dos textBoxes tiene más líneasvar 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'------------------------------------------------------------------------------OptionStrictOnOptionInferOnImports Microsoft.VisualBasic
Imports System
Imports System.Windows
Imports System.Windows.Controls
ClassMainWindowPrivate inicializando AsBoolean = TruePrivateSub Window_Loaded(sender AsObject,
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)
EndSub'''<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>PrivateSub Sv_ScrollChanged(sender AsObject,
e As ScrollChangedEventArgs)
If inicializando ThenReturn
inicializando = TrueDim 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)
EndIf
inicializando = FalseEndSub'''<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>PrivateSub TxtCodigo_TextChanged(sender AsObject,
e As TextChangedEventArgs)
If inicializando ThenReturn' Contar las líneasDim linIzq = txtIzquierda.LineCount
Dim linDer = txtDerecha.LineCount
' Comprobar cuál de los dos textBoxes tiene más líneasDim 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
NextEndSubEndClass
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 con el código de Visual Basic y C# (solución de Visual Studio 2017)
El ZIP contiene la solución para Visual Studio con 4 ejemplos, dos del post anterior y los dos (VB y C#) de este.
Pues eso… que en el programa que publiqué ayer en mi sitio: Compilar y ejecutar (desde una aplicación) utilizo dos textbox con desplazamiento vertical sincronizado, es decir, cuando me desplazo por uno de los TextBox (sea pulsando en la barra de scroll o con la rueda del ratón o con pulsaciones de teclas) el otro se desplaza también.
Los preliminares
Aquí te voy a mostrar dos ejemplos, el primero como el de la aplicación esa que te he comentado antes (que solo usa dos controles TextBox, uno de ellos sin mostrar las barras de desplazamiento) (ver figura 1) y el otro en el que hay dos TextBox que se sincronizan tanto vertical como horizontalmente y un tercero que solo se sincroniza verticalmente (ver figura 2).
Figura 1. Dos textbox sincronizados con el scroll verticalFigura 3. Tres textbox sincronizados, dos horizontalmente y los tres verticalmente
¿Qué hacer para sincronizar los TextBox?
Lo que necesitamos es detectar que se hace scroll (desplazamiento) en los controles a sincronizar. Yo he usado el objeto ScrollViewer que contiene un TextBox y por tanto se encarga de sincronizar el contenido del TextBox con las barras de desplazamiento.
Debemos añadir un evento que detecte que se ha desplazado la barra de scroll (por no repetir desplazamiento) y esto lo podemos hacer en el diseñador XAML indicando que queremos crear el evento que corresponda, en nuestro caso ScrollChanged.
Para hacerlo, escribimos el nombre del evento y el diseñador de WPF de Visual Studio nos muestra la opción de agregar uno nuevo o usar uno que ya exista (cuando muestra métodos de evento existentes será porque la firma es la misma, es decir, tiene los mismos tipos de argumentos). En la figura 3 puedes ver lo que te estoy comentado.
Figura 3. Indicamos el evento que queremos usar en un control.
El evento de sincronización, perdón de desplazamiento, cambiado (ScrollChanged) lo tenemos que poner en todos los ScrollViewer que tengamos.
Y para nuestro ejemplo, solo necesitamos un método de evento, en él haremos las comprobaciones correspondientes para saber qué ScrollViewer ha lanzado el evento.
Nota: Para poder detectar que ha cambiado el desplazamiento (con scroll en las barras) debemos poner el control que queramos (TextBox, ListBox, RichTextBox, etc.) dentro de un ScrollViewer. Ya que esos controles no implementan el evento ScrollChanged.
Si no quieres detectar ese evento, entonces no hace falta que lo pongas dentro de un ScrollViewer.
El código de VB y C# para sincronizar los TextBox
Aquí te muestro el código del evento ScrollChanged que hemos definido en los dos ScrollViewer que tenemos. Este es el del ejemplo 1 (Figura 1) en el que solo tenemos dos cajas de texto, una para mostrar los números (así ves mejor que las líneas del otro TextBox están sincronizadas).
'''<summary>
''' Sincronizar el scroll vertical del código y las filas
'''</summary>
PrivateSub SvFilas_ScrollChanged(sender AsObject,
e As ScrollChangedEventArgs)
If inicializando ThenReturn
inicializando = True
Dim sv = TryCast(sender, ScrollViewer)
If sv Is svFilas Then
svCodigo.ScrollToVerticalOffset(e.VerticalOffset)
Else
svFilas.ScrollToVerticalOffset(e.VerticalOffset)
EndIf
inicializando = False
EndSub
///<summary>
/// Sincronizar el scroll vertical del código y las filas
///</summary>
privatevoid SvFilas_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (inicializando)
return;
inicializando = true;
var sv = sender as ScrollViewer;
if (sv == svFilas)
svCodigo.ScrollToVerticalOffset(e.VerticalOffset);
else
svFilas.ScrollToVerticalOffset(e.VerticalOffset);
inicializando = false;
}
Fíjate que tengo una variable llamada inicializando que he declarado a nivel de clase, esta nos servirá para que no se repita en cascada el evento. Yo normalmente la pongo a False cuando el formulario ha terminado de cargarse.
Como puedes comprobar lo que hacemos es cambiar o asignar el valor de la propiedad ScrollToVerticalOffset del otro ScrollViewer.
¡Así es como se sincronizan!
Ajustar el contenido del TextBox con los números de líneas
En el ejemplo que estoy usando una de las cajas de texto muestra el número de línea en el que está actualmente. Esas líneas las tendremos que modificar en consonancia con las líneas que tenga el otro TextBox.
Así que, tendremos que detectar cuándo se cambia el contenido de la caja de textos normal y agregar las líneas a la otra.
Lo primero que tenemos que hacer es añadir un método para detectar que el contenido de la caja de textos txtCodigo ha cambiado y asignar a la otra los números para las líneas.
El evento en cuestión es TextChanged y el código a usar puede ser el que te muestro a continuación.
El código de VB y C# para procesar el evento TextChanged
En el siguiente código contaremos las líneas del TextBox utilizando la propiedad LineCount, la cual indica cuántas líneas tiene el control, al menos si es multilínea, es decir, que tenga más de una línea, eso se consigue con la propiedad TextWrapping con un valor Wrap o WrapWithOverflow, el valor predeterminado es NoWrap, que no vale para este ejemplo.
Nota: Los TextBox multilínea de WindowsForms tienen una propiedad Lines que devuelve un array de tipo String con el texto del control. cosa que no ocurre con los TextBox de WPF que solo puedes saber el número de líneas que tiene.
'''<summary>
''' Al cambiar el texto del código actualizar el número de líneas
'''</summary>
PrivateSub TxtCodigo_TextChanged(sender AsObject,
e As TextChangedEventArgs)
' Contar las líneas
Dim lin = txtCodigo.LineCount
txtFilas.Text = ""
For i = 1 To lin
' Indentar el texto a la derecha
txtFilas.Text &= i.ToString("0").PadLeft(4) & vbCr
Next
EndSub
///<summary>
/// Al cambiar el texto del código actualizar el número de líneas
///</summary>
privatevoid TxtCodigo_TextChanged(object sender, TextChangedEventArgs e)
{
// Contar las líneas
var lin = txtCodigo.LineCount;
txtFilas.Text = "";
for (var i = 1; i <= lin; i++)
// Indentar el texto a la derecha
txtFilas.Text += i.ToString("0").PadLeft(4) + "\r";
}
El PadLeft es para añadir espacios a la izquierda de forma que tenga la cantidad que indicamos, de esta forma los números se alinean bien. No te lo puedo demostrar con las imágenes que he puesto, ya que ninguna empieza por líneas con menos de 2 cifras
El código XAML completo
Aquí tienes todo el código Xaml para que veas cómo están configurados los controles y de paso te sirva para tener una idea (si no la tienes ya) de cómo ajustar los controles al Grid.
<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_dos_textbox_vb" mc:Ignorable="d" Title="WPF Scroll sincronizado con dos TextBox (VB)" Height="450" Width="800" WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip" WindowStyle="ThreeDBorderWindow" Loaded="Window_Loaded"><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="Auto" MinWidth="36"/><ColumnDefinition Width="Auto"/><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,0" HorizontalContentAlignment="Center" Content="Dos TextBox sincronizados 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="svFilas" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" ScrollChanged="SvFilas_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" FontFamily="Consolas" FontSize="12" Foreground="DarkCyan" AllowDrop="False" Focusable="False" IsTabStop="False"/></ScrollViewer><ScrollViewer x:Name="svCodigo" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="2" Grid.RowSpan="2" ScrollChanged="SvFilas_ScrollChanged"><TextBox x:Name="txtCodigo" Margin="0,0,2,0" Padding="4,0" TextWrapping="Wrap" FontFamily="Consolas" FontSize="12" Text="Aquí irá el texto a mostrar" AcceptsReturn="True" AcceptsTab="True" TextChanged="TxtCodigo_TextChanged"/></ScrollViewer><Label x:Name="lblInfo2" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Background="{DynamicResource {x:Static SystemColors.InfoBrushKey}}" Margin="4,0" HorizontalContentAlignment="Center" Content="Mueve el scroll vertical o la rueda del ratón en el texto, el de la izquierda no es visible, pero puedes desplazarte por el contenido."/></Grid></Window>
El código de VB y C# ya está casi completo y como lo que yo he añadido es un texto de ejemplo que ocupa mucho, pues… me lo ahorro. Ese código lol asigno en el evento Load (Window_Loaded).
Y esto es todo. Espero que lo sincronices bien
Que no, que no me he olvidado del otro ejemplo con tres controles y la sincronización tanto vertical como horizontal. Pero lo dejo para otro post, que este ya está bien cargadito
En el otro post (pondré aquí el enlace) te pondré un ZIP con el código completo tanto para Visual Basic como C# en una solución para Visual Studio 2017 con .NET 4.7.2.
Ahora voy a comer algo y después sigo con el otro post o entrada del blog. ¡Buen provecho! ¡Gracias!