Etiqueta: WPF

  • Abrir y guardar archivos usando RichTextBox para WPF

    Pues eso… hoy te voy a explicar cómo implementar las opciones de abrir y guardar archivos usando un control RichTextBox para WPF.

    Actualizado (o nota del 15/Ene/19)
    En realidad actualizado no está, salvo este comentario del martes 15 de enero.

    Donde está la actualización es en el post que he publicado hoy: Indicar el Encoding al guardar el contenido de un RichTextBox de WPF, y es que resulta que con el código tal como te lo muestro aquí, si decides usar el formato Xaml (ya sabes: lo guarda como párrafos, etc.) y tu texto tiene tildes (vocales acentuadas), pues… resulta que no lo hace bien.

    Así que… te recomiendo que veas el post de hoy y si te has descargado el zip con los proyectos, modifiques el código. Gracias

    Como ya vimos en el post anterior (Leer el contenido como cadena y asignar un valor nuevo) usaremos tres opciones de formatos admitidos por RichTextBox de WPF. ¿Los recuerdas? Vale, te los resumo de nuevo:

    • Rtf el contenido debe estar en formato RTF.
    • Xaml el contenido debe estar en formato Xaml pero el que se puede poner en un RichTextBox o FlowDocument, cuando lo guardas lo pone dentro de un elemento Section. Es decir, no vale cualquier código Xaml y menos el que define un objeto Window.
    • Text Formato de texto plano, sin ningún tipo de formato.

    Contenido de la ventana principal (MainWindow)

    En la aplicación he puesto un control RichTextBox (de eso se trata este ejemplo, ¿no?) y además de una etiqueta para mostrar la información del archivo activo, también hay un menú con un elemento (Archivo) con las opciones de Abrir, Guardar como y Salir.

    En esos tres submenús he puesto imágenes, éstas están descargadas de las que pone Visual Studio 2017 a nuestra disposición, realmente las que utiliza el propio Visual Studio 2017. El enlace para descargar todas esas imágenes (son un montón, lo que yo te diga) es este: Biblioteca de imágenes de Visual Studio. En ese enlace te explica qué tipos de imágenes contiene y más cosillas, aparte, claro del enlace para descargarlas.

    Este es el código XAMl de la ventana principal (recuerda que es el mismo diseño para Visual Basic que para C#, lo único que cambia es el espacio de nombres usado en cada proyecto, más abajo te pongo el ZIP con el código completo para Visual Basic y para C# en una solución de Visual Studio 2017.

    <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_Abrir_y_guardar_en_RichTextBox_vb"
            mc:Ignorable="d"
            Title="Abrir y guardar archivos en un RichTextBox (VB)" 
            WindowStartupLocation="CenterScreen"
            ResizeMode="CanResizeWithGrip" 
            WindowStyle="ThreeDBorderWindow"
            Loaded="Window_Loaded" Closing="Window_Closing"
            Height="450" Width="800"
            Icon="Images/RichTextBox_16x.png">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition Width="Auto" MinWidth="100" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" MinHeight="24"/>
                <RowDefinition />
                <RowDefinition />
                <RowDefinition Height="Auto" MinHeight="24" />
            </Grid.RowDefinitions>
            <Menu Grid.Row="0" Grid.Column="0" 
                  Grid.ColumnSpan="2" Background="AliceBlue">
                <MenuItem Header="_Archivo" ToolTip="Abrir, Guardar, Salir">
                    <MenuItem x:Name="mnuAbrir" Header="_Abrir..."
                                      Click="MnuAbrir_Click">
                        <MenuItem.Icon>
                            <Image Source="Images\OpenFile_16x.png" />
                        </MenuItem.Icon>
                    </MenuItem>
                    <MenuItem x:Name="mnuGuardar" Header="_Guardar cómo..."
                                      Click="MnuGuardar_Click">
                        <MenuItem.Icon>
                            <Image Source="Images\Save_16x.png" />
                        </MenuItem.Icon>
                    </MenuItem>
                    <Separator />
                    <MenuItem x:Name="mnuSalir" Header="_Salir" Click="MnuSalir_Click">
                        <MenuItem.Icon>
                            <Image Source="Images\Close_16x.png" />
                        </MenuItem.Icon>
                    </MenuItem>
                </MenuItem>
            </Menu>
            <RichTextBox x:Name="rtb" 
                         BorderThickness="2"
                         AcceptsTab="True" AcceptsReturn="True"
                         Grid.Column="0" Grid.Row="1" 
                         Grid.ColumnSpan="2" Grid.RowSpan="2"
                         TextChanged="Rtb_TextChanged">
                <FlowDocument />
            </RichTextBox>
            <Label x:Name="lblStatus" Content="Información..."
                   Padding="4,2,4,2"
                   Grid.Column="0" Grid.Row="3" 
                   Grid.ColumnSpan="2"
                   Background="{DynamicResource {x:Static SystemColors.InfoBrushKey}}" />
        </Grid>
    </Window>
    

    Las definiciones de las filas y columnas del Grid nos dan espacio (no demasiado ancho) para la primera fila (la de los menús) y la última (la de la etiqueta de estado).

    La columna esa que tengo con MinWidth a 100 era para poder poner el típico botón Salir, pero como ya lo tengo en el menú de Archivo, ¿pa qué ponerlo? pero ya que estaba… la he dejado

    Las imágenes usadas como recurso están en una carpeta llamada Images y aparte de las mostradas en los menús, hay otra para usarla como icono de la ventana: RichTextBox_16x.png.

    Nota:
    Fíjate que en la definición de la ventana (Window) se asigna a la propiedad Icon, pero en realidad no es un icono. Te lo digo por si lo quieres usar como icono de la aplicación. En ese caso tendrás que crear un icono nuevo, añadir una nueva imagen (o tipo de imagen) de 16×16 con 24 bits, copiar la imagen con un programa, por ejemplo el Paint que se incluye con Windows y después pegarla en ese icono creado en Visual Studio.

    Vale, te lo explico paso a paso.

    Crear un icono con Visual Studio a partir de una imagen

    Lo he puesto como post separado para no cargar más de la cuenta este: Crear un icono con Visual Studio a partir de una imagen.

    Fíjate que en el código XAML no he indicado la visibilidad de las barras de desplazamiento del control RichTextBox, por tanto no se muestran (tampoco está incluido en un control ScrollViewer). Si quieres que se muestren tanto la horizontal como la vertical, tendrás que indicarlo expresamente.

    HorizontalScrollBarVisibility="Auto"
    VerticalScrollBarVisibility="Auto"
    

    Si le asignas un valor Auto se mostrarán según sea necesario (al menos la vertical, ya que la horizontal siempre se mostrará). Si quieres que siempre sean visible las dos, en vez de Auto indica el valor Visible.

    El código para abrir un archivo y asignar el contenido en el RichTextBox

    A continuación te muestro las funciones (tanto para VB.NET como para C#) del método usado para abrir un archivo y asignarlo al contenido del control RichTextBox.

    Nota:
    El código mostrado en el método LoadRtfFormat lo he convertido a partir de un ejemplo mostrado en la documentación en línea de Visual Studio.

    ''' <summary>
    ''' Adaptado del ejemplo de la documentación de Microsoft
    ''' https://docs.microsoft.com/es-es/dotnet/framework/wpf/
    '''     controls/richtextbox-overview
    ''' </summary>
    Private Function LoadRtfFormat(_fileName As String,
                                   richTB As RichTextBox,
                                   formato As String) As Boolean
        Dim abierto = False
    
        If File.Exists(_fileName) Then
            Dim range = New TextRange(richTB.Document.ContentStart,
                                      richTB.Document.ContentEnd)
            Using sr As New StreamReader(_fileName, Encoding.Default, True)
                Try
                    ' leer el contenido para admitir tildes, etc.   (09/Ene/19)
                    Dim texto = sr.ReadToEnd
                    Dim stream = New MemoryStream(Encoding.UTF8.GetBytes(texto))
    
                    range.Load(stream, formato)
                    abierto = True
                Catch ex As Exception
                    MessageBox.Show("Error el formato no es válido" & vbCrLf &
                                    ex.Message,
                                    $"Abrir {formato}",
                                    MessageBoxButton.OK,
                                    MessageBoxImage.Asterisk)
                End Try
            End Using
        End If
    
        Return abierto
    End Function
    
    Private Function LoadRtf(ByVal _fileName As String,
                             richTB As RichTextBox) As Boolean
        Return LoadRtfFormat(_fileName, richTB, DataFormats.Rtf)
    End Function
    
    ' Al cargar como Xaml da error el Visual Studio
    ' ya que lo trata como una clase
    ' y esto en realidad es para abrir con el XAML generado,
    '   con los elementos Paragraph, Bold, Run, etc.
    Private Function LoadRtfXaml(ByVal _fileName As String,
                                 richTB As RichTextBox) As Boolean
        Return LoadRtfFormat(_fileName, richTB, DataFormats.Xaml)
    End Function
    
    ''' <summary>
    ''' El formato texto abrirlo directamente
    ''' Aunque funciona igual que llamando a LoadRtfFormat
    ''' </summary>
    Private Function LoadRtfText(ByVal _fileName As String,
                                 richTB As RichTextBox) As Boolean
        'Return LoadRtfFormat(_fileName, richTB, DataFormats.Text)
    
        Dim abierto = False
    
        If File.Exists(_fileName) Then
            Dim range = New TextRange(richTB.Document.ContentStart,
                                      richTB.Document.ContentEnd)
            Using sr As New StreamReader(_fileName, Encoding.Default, True)
                Try
                    ' leer el contenido para admitir tildes, etc.   (09/Ene/19)
                    Dim texto = sr.ReadToEnd
    
                    Dim textRange = New TextRange(richTB.Document.ContentStart,
                                                  richTB.Document.ContentEnd)
                    textRange.Text = texto
    
                    abierto = True
                Catch ex As Exception
                    MessageBox.Show("Error el formato no es válido" & vbCrLf &
                                    ex.Message,
                                    "Abrir Text",
                                    MessageBoxButton.OK,
                                    MessageBoxImage.Asterisk)
                End Try
            End Using
        End If
    
        Return abierto
    End Function
    
    /// <summary>
    /// Adaptado del ejemplo de la documentación de Microsoft
    /// https://docs.microsoft.com/es-es/dotnet/framework/wpf/
    ///     controls/richtextbox-overview
    /// </summary>
    private bool LoadRtfFormat(string _fileName, RichTextBox richTB, string formato)
    {
        var abierto = false;
    
        if (File.Exists(_fileName))
        {
            var range = new TextRange(richTB.Document.ContentStart, 
                                      richTB.Document.ContentEnd);
    
            using (StreamReader sr = new StreamReader(_fileName, 
                                                      Encoding.Default, 
                                                      true))
            {
                try
                {
                    // leer el contenido para admitir tildes, etc.   (09/Ene/19)
                    var texto = sr.ReadToEnd();
                    var stream = new MemoryStream(Encoding.UTF8.GetBytes(texto));
    
                    range.Load(stream, formato);
                    abierto = true;
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error el formato no es válido\r\n"  + 
                                    ex.Message, $"Abrir {formato}", 
                                    MessageBoxButton.OK, 
                                    MessageBoxImage.Asterisk);
                }
            }
        }
    
        return abierto;
    }
    
    private bool LoadRtf(string _fileName, RichTextBox richTB)
    {
        return LoadRtfFormat(_fileName, richTB, DataFormats.Rtf);
    }
    
    // Al cargar como Xaml da error el Visual Studio
    // ya que lo trata como una clase
    // y esto en realidad es para abrir con el XAML generado,
    // con los elementos Paragraph, Bold, Run, etc.
    private bool LoadRtfXaml(string _fileName, RichTextBox richTB)
    {
        return LoadRtfFormat(_fileName, richTB, DataFormats.Xaml);
    }
    
    /// <summary>
    /// El formato texto abrirlo directamente
    /// Aunque funciona igual que llamando a LoadRtfFormat
    /// </summary>
    private bool LoadRtfText(string _fileName, RichTextBox richTB)
    {
        var abierto = false;
    
        if (File.Exists(_fileName))
        {
            var range = new TextRange(richTB.Document.ContentStart, 
                                      richTB.Document.ContentEnd);
    
            using (StreamReader sr = new StreamReader(_fileName, 
                                                      Encoding.Default, 
                                                      true))
            {
                try
                {
                    // leer el contenido para admitir tildes, etc.   (09/Ene/19)
                    var texto = sr.ReadToEnd();
    
                    var textRange = new TextRange(richTB.Document.ContentStart, 
                                                  richTB.Document.ContentEnd);
                    textRange.Text = texto;
    
                    abierto = true;
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error el formato no es válido\r\n" + 
                                    ex.Message, 
                                    "Abrir Text", 
                                    MessageBoxButton.OK, 
                                    MessageBoxImage.Asterisk);
                }
            }
        }
    
        return abierto;
    }
    

    Nota:
    El código del método LoadRtfText se podría haber reducido haciendo una llamada al método LoadRtfFormat e indicando como último argumento DataFormats.Text. Pero… ese también vale, por si quieres verlo de la forma tradicional.

    Te recuerdo que el tipo XAML no es un archivo de diseño normal XAML / WPF si no el formato XAML del contenido del RichTextBox.

    Para llamar a ese método lo haremos desde el método de evento MnuAbrir_Click que es el que usará la aplicación cuando el usuario pulse en el menú Abrir.

    Veamos el código para VB y C# y te explico un par de detalles.

    Private Sub MnuAbrir_Click(sender As Object, e As RoutedEventArgs)
        If rtbModificado Then
            If MessageBox.Show("El texto está modificado," & vbCrLf &
                               "¿seguro que quieres abir?",
                               "Texto Modificado",
                               MessageBoxButton.YesNo,
                               MessageBoxImage.Question) = MessageBoxResult.No Then
                Exit Sub
            End If
        End If
        Dim oFD As New OpenFileDialog
        oFD.Filter = "Formato RTF|*.rtf|" &
                     "Formato Xaml del contenido RTF|*.xaml|" &
                     "Código C# y VB|*.cs;*.vb|" &
                     "Texto (*.txt)|*.txt|Todos (*.*)|*.*"
    
        oFD.Title = "Selecciona el archivo"
        oFD.InitialDirectory = My.Computer.FileSystem.SpecialDirectories.MyDocument
        oFD.FileName = ""
        If oFD.ShowDialog() Then
            Dim ext = System.IO.Path.GetExtension(oFD.FileName).ToLower()
            Select Case ext
                Case ".rtf"
                    LoadRtf(oFD.FileName, rtb)
                Case ".xaml"
                    LoadRtfXaml(oFD.FileName, rtb)
                Case Else
                    LoadRtfText(oFD.FileName, rtb)
            End Select
    
            rtbModificado = False
            lblStatus.Content = $"Texto cargado de: {oFD.FileName}"
        End If
    End Sub
    
    private void MnuAbrir_Click(object sender, RoutedEventArgs e)
    {
        if (rtbModificado)
        {
            if (MessageBox.Show("El texto está modificado,\r\n" + 
                                "¿seguro que quieres abir?", 
                                "Texto Modificado", 
                                MessageBoxButton.YesNo, 
                                MessageBoxImage.Question) == MessageBoxResult.No)
                return;
        }
        OpenFileDialog oFD = new OpenFileDialog();
        oFD.Filter = "Formato RTF|*.rtf|" + "Formato Xaml del contenido RTF|*.xaml|" + 
                     "Código C# y VB|*.cs;*.vb|" + "Texto (*.txt)|*.txt|Todos (*.*)|*.*";
    
        oFD.Title = "Selecciona el archivo";
        // oFD.InitialDirectory = My.Computer.FileSystem.SpecialDirectories.MyDocuments;
        oFD.InitialDirectory = Environment.GetFolderPath(
                                            Environment.SpecialFolder.MyDocuments);
        oFD.FileName = "";
        if (oFD.ShowDialog() == true)
        {
            var ext = System.IO.Path.GetExtension(oFD.FileName).ToLower();
            switch (ext)
            {
                case ".rtf":
                    {
                        LoadRtf(oFD.FileName, rtb);
                        break;
                    }
    
                case ".xaml":
                    {
                        LoadRtfXaml(oFD.FileName, rtb);
                        break;
                    }
    
                default:
                    {
                        LoadRtfText(oFD.FileName, rtb);
                        break;
                    }
            }
    
            rtbModificado = false;
            lblStatus.Content = $"Texto cargado de: {oFD.FileName}";
        }
    }
    

    En este método se comprueba si el texto está modificado, de ser así, da la oportunidad para guardarlo antes de abrir el nuevo.

    El método OpenFileDialog está definido en el espacio de nombres Microsoft.Win32, si prefieres el clásico de Windows Forms, tendrás que agregar una referencia al proyecto a esa biblioteca de WinForms.
    Para los de Visual Basic no hay gran diferencia, salvo que el método ShowDialog devuelve True o False según se haya aceptado o cancelado. En el caso de C# hay que comprobarlo con el signo de igualdad, ya que el valor devuelto por el método ShowDialog es de tipo bool?.

    En cuanto al directorio de inicio (InitialDirectory) con Visual Basic he utilizado el objeto My, para C# tengo una clase que simula algunas de las características de My, concretamente My.Properties, My.Application.Info y My.Computer.FileSystem.SpelciaDirectories, pero he preferido usar aquí la llamada directa a las clases de .NET (que es lo que supongo que harán las definiciones correspondientes de Visual Basic). Concretamente la llamada al método Environment.GetFolderPath al que le pasamos el valor MyDocuments de la enumeración Environment.SpecialFolder y devuelve el valor como una cadena, que es lo que necesitamos aquí.

    El tipo de archivo lo dará la extensión del mismo y eso hago, una comprobación según la extensión, diferenciando los tipos Rtf y Xaml del resto, que los considero como texto (Text).

    Finalmente mostramos en la etiqueta de información el nombre del archivo e indicamos que el texto no se ha modificado.

    El código para guardar en un archivo el contenido del RichTextBox

    Ahora le toca la parte de guardar. El método principal lo he llamado SaveRtfFormat que también está en el ejemplo sobre RichTextBox que te he indicado antes.

    Veamos el código para Visual Basic y C#.

    ''' <summary>
    ''' Adaptado del ejemplo de la documentación de Microsoft
    ''' https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
    '''     richtextbox-overview
    ''' </summary>
    Private Function SaveRtfFormat(_fileName As String,
                                   richTB As RichTextBox,
                                   formato As String) As Boolean
        Dim range As TextRange
        Dim guardado As Boolean = False
    
        range = New TextRange(richTB.Document.ContentStart, richTB.Document.ContentEnd)
        Using fStream As New FileStream(_fileName, FileMode.Create)
            Try
                range.Save(fStream, formato)
                guardado = True
            Catch ex As Exception
                MessageBox.Show("Error el formato no es válido" & vbCrLf &
                                ex.Message,
                                $"Guardar {formato}",
                                MessageBoxButton.OK,
                                MessageBoxImage.Asterisk)
            End Try
    
            fStream.Close()
        End Using
    
        Return guardado
    End Function
    
    Private Function SaveRtf(ByVal _fileName As String,
                             richTB As RichTextBox) As Boolean
        Return SaveRtfFormat(_fileName, richTB, DataFormats.Rtf)
    End Function
    Private Function SaveRtfXaml(ByVal _fileName As String,
                                 richTB As RichTextBox) As Boolean
        Return SaveRtfFormat(_fileName, richTB, DataFormats.Xaml)
    End Function
    
    ''' <summary>
    ''' El formato Text lo guardo como texto normal
    ''' </summary>
    Private Function SaveRtfText(ByVal _fileName As String,
                            richTB As RichTextBox) As Boolean
        'Return SaveRtfFormat(_fileName, richTB, DataFormats.Text)
    
        Dim guardado = False
    
        Try
            Dim texto = getRtbText(richTB)
            Using sw As New StreamWriter(_fileName, False, Encoding.Default)
                sw.WriteLine(texto)
            End Using
    
            guardado = True
        Catch ex As Exception
            MessageBox.Show("Error al guardar:" & vbCrLf &
                            ex.Message,
                            "Guardar Text",
                            MessageBoxButton.OK,
                            MessageBoxImage.Asterisk)
        End Try
    
        Return guardado
    End Function
    
    /// <summary>
    /// Adaptado del ejemplo de la documentación de Microsoft
    /// https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
    ///     richtextbox-overview
    /// </summary>
    private bool SaveRtfFormat(string _fileName, RichTextBox richTB, string formato)
    {
        TextRange range;
        bool guardado = false;
    
        range = new TextRange(richTB.Document.ContentStart, 
                              richTB.Document.ContentEnd);
    
        using (FileStream fStream = new FileStream(_fileName, 
                                                   FileMode.Create))
        {
            try
            {
                range.Save(fStream, formato);
                guardado = true;
            }
            catch (Exception ex)
            {
                MessageBox.Show("Error el formato no es válido\r\n"+ 
                                ex.Message, 
                                $"Guardar {formato}", 
                                MessageBoxButton.OK, 
                                MessageBoxImage.Asterisk);
            }
    
            fStream.Close();
        }
    
        return guardado;
    }
    
    private bool SaveRtf(string _fileName, RichTextBox richTB)
    {
        return SaveRtfFormat(_fileName, richTB, DataFormats.Rtf);
    }
    private bool SaveRtfXaml(string _fileName, RichTextBox richTB)
    {
        return SaveRtfFormat(_fileName, richTB, DataFormats.Xaml);
    }
    
    /// <summary>
    /// El formato Text lo guardo como texto normal
    /// </summary>
    private bool SaveRtfText(string _fileName, RichTextBox richTB)
    {
        var guardado = false;
    
        try
        {
            var texto = getRtbText(richTB);
            using (StreamWriter sw = new StreamWriter(_fileName, 
                                                      false, 
                                                      Encoding.Default))
            {
                sw.WriteLine(texto);
            }
    
            guardado = true;
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error al guardar:\r\n" + 
                            ex.Message, 
                            "Guardar Text", 
                            MessageBoxButton.OK, 
                            MessageBoxImage.Asterisk);
        }
    
        return guardado;
    }
    
    /// <summary>
    /// Extrae el texto de un RichTextBox y lo devuelve como una cadena.
    /// De un ejemplo en C# de:
    /// https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
    ///     how-to-extract-the-text-content-from-a-richtextbox
    /// </summary>
    private string getRtbText(RichTextBox rtb)
    {
        var textRange = new TextRange(rtb.Document.ContentStart,
                                      rtb.Document.ContentEnd);
        return textRange.Text;
    }
    

    Como en el método SaveRtfText lo hago directamente, llamo al método getRtbText que ya vimos en el post anterior.

    Estos métodos los llamaremos desde el método de evento relacionado con el evento Click del menú Guardar como…, tal como te muestro a continuación.

    Private Sub MnuGuardar_Click(sender As Object, e As RoutedEventArgs)
        Dim oFD As New SaveFileDialog
        oFD.Filter = "Formato RTF|*.rtf|" &
                     "Formato Xaml del contenido RTF|*.xaml|" &
                     "Código C# y VB|*.cs;*.vb|" &
                     "Texto (*.txt)|*.txt|Todos (*.*)|*.*"
    
        oFD.Title = "Selecciona el archivo para guardar el contenido RTF"
        oFD.InitialDirectory = My.Computer.FileSystem.SpecialDirectories.MyDocuments
        oFD.FileName = ""
        If oFD.ShowDialog() Then
            Dim ext = System.IO.Path.GetExtension(oFD.FileName).ToLower()
            Select Case ext
                Case ".rtf"
                    SaveRtf(oFD.FileName, rtb)
                Case ".xaml"
                    SaveRtfXaml(oFD.FileName, rtb)
                Case Else
                    SaveRtfText(oFD.FileName, rtb)
            End Select
    
            rtbModificado = False
            lblStatus.Content = $"Texto guardado como: {oFD.FileName}"
        End If
    
    End Sub
    
    private void MnuGuardar_Click(object sender, RoutedEventArgs e)
    {
        SaveFileDialog oFD = new SaveFileDialog();
        oFD.Filter = "Formato RTF|*.rtf|" + "Formato Xaml del contenido RTF|*.xaml|" + 
                     "Código C# y VB|*.cs;*.vb|" + "Texto (*.txt)|*.txt|Todos (*.*)|*.*";
    
        oFD.Title = "Selecciona el archivo para guardar el contenido RTF";
        // oFD.InitialDirectory = My.Computer.FileSystem.SpecialDirectories.MyDocuments;
        oFD.InitialDirectory = Environment.GetFolderPath(
                                            Environment.SpecialFolder.MyDocuments);
        oFD.FileName = "";
        if (oFD.ShowDialog() == true)
        {
            var ext = System.IO.Path.GetExtension(oFD.FileName).ToLower();
            switch (ext)
            {
                case ".rtf":
                    {
                        SaveRtf(oFD.FileName, rtb);
                        break;
                    }
    
                case ".xaml":
                    {
                        SaveRtfXaml(oFD.FileName, rtb);
                        break;
                    }
    
                default:
                    {
                        SaveRtfText(oFD.FileName, rtb);
                        break;
                    }
            }
    
            rtbModificado = false;
            lblStatus.Content = $"Texto guardado como: {oFD.FileName}";
        }
    }
    

    Aquí nada especial que contar, salvo que en vez de usar la clase OpenFileDialog usamos SaveFileDialog, el resto, es lo mismo que te expliqué antes.

    Y este es el código principal de la aplicación, a falta de la comprobación al cerrar el formulario, perdón ventana, principal que se comprueba si el código se ha modificado y el evento TextChanged del control RichTextBox.

    Private Sub Rtb_TextChanged(sender As Object, e As TextChangedEventArgs)
        If inicializando Then Return
    
        rtbModificado = True
    End Sub
    
    
    
    Private Sub Window_Closing(sender As Object, e As CancelEventArgs)
        ' Comprobar si están modificados
        If rtbModificado Then
            If MessageBox.Show("El texto está modificado," & vbCrLf &
                               "¿Quieres guardarlo?",
                               "Texto Modificado",
                               MessageBoxButton.YesNo,
                               MessageBoxImage.Question) = MessageBoxResult.Yes Then
                rtb.Focus()
                MnuGuardar_Click(mnuGuardar, Nothing)
            End If
        End If
    End Sub
    
    private void Rtb_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (inicializando)
            return;
    
        rtbModificado = true;
    }
    
    private void Window_Closing(object sender, CancelEventArgs e)
    {
        // Comprobar si est\f2án modificados
        if (rtbModificado)
        {
            if (MessageBox.Show("El texto está modificado,\r\n" + 
                                "¿Quieres guardarlo?", 
                                "Texto Modificado", 
                                MessageBoxButton.YesNo, 
                                MessageBoxImage.Question) == MessageBoxResult.Yes)
            {
                rtb.Focus();
                MnuGuardar_Click(mnuGuardar, null);
            }
        }
    }
    

    Ah, sí, una cosilla que he puesto en el evento de carga de la ventana (evento Window_Loaded) en el que asigno un ancho grande al documento interno del RichTextBox con idea de que no se muestren las líneas cortadas (ver figuras 1 y 2).

    Figura 1. Si no asignamos un valor grande a Document.PageWidth, las líneas largas se mostrarán partidas
    Figura 1. Si no asignamos un valor grande a Document.PageWidth, las líneas largas se mostrarán partidas
    Figura 2. Al asignar un valor alto a Document.PageWidth las líneas no se cortan

    Nota:
    El archivo mostrado en las figuras 1 y 2 es uno en formato RTF coloreado por mi aplicación gsColorear.

    Lo que hago es asignar un valor 2000 a la propiedad PageWidth del objeto Document del RichTextBox (si crees que tendrás líneas de más de 2000, pues ponle un valor mayor).

    Ese ejemplo (o el truco para hacer eso) lo saqué (creo) de un foro de StackOverflow:
    C#/WPF: Disable Text-Wrap of RichTextBox.

    Private Sub Window_Loaded(sender As Object,
                              e As RoutedEventArgs)
        ' Para que no corte las líneas
        rtb.Document.PageWidth = 2000
    
        lblStatus.Content = My.Application.Info.Copyright & " - " &
                            My.Application.Info.Description
    
        inicializando = False
    
        rtb.Focus()
    
    End Sub
    
    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        // Para mostrar la info del Copyright y Description
        FileVersionInfo fvi;
        System.Reflection.Assembly ensamblado;
        ensamblado = System.Reflection.Assembly.GetExecutingAssembly();
        fvi = FileVersionInfo.GetVersionInfo(ensamblado.Location);
    
        // Para que no corte las líneas
        rtb.Document.PageWidth = 2000;
    
        //lblStatus.Content = My.Application.Info.Copyright + " - " +
        //                    My.Application.Info.Description;
    
        lblStatus.Content = fvi.LegalCopyright + " - " +
                            fvi.Comments;
    
        inicializando = false;
    
        rtb.Focus();
    }
    

    Nota:
    Esto ocurre (que se corten las líneas) porque el control RichTextBox no tiene una propiedad TextWrapping como ocurre con el control TextBox.

    Eso mismo se puede hacer asignando el valor directamente en la definición del objeto FlowDocument del RichTextBox:

    <FlowDocument PageWidth="2000" />
    

    Resaltar en el código para C# que como no tiene el objeto My de Visual Basic, para acceder a la información del Copyright y Description uso llamadas directas a la información ofrecida por la clase FileVersionInfo a partir del ensamblado actual.

    Pues ya está, esto es todo… para la próxima ocasión veremos cómo cambiar algunas propiedades del contenido del RichTextBox, como por ejemplo el tipo y tamaño de la letra y algunas cosillas más, ya que si pruebas a abrir un archivo de texto plano, pues… como que no se ve muy bien, en la figura 3 tienes un ejemplo de cómo se vería un archivo de C# (sin colorear).

    Figura 3. Así se vería el contenido de un archivo abierto como texto

    Nos vemos.
    Guillermo

    Aquí tienes el código completo para poder usarla (una vez descomprimido) con la solución para Visual Studio 2017. Están los dos proyectos, el de Visual Basic y el de C# (como de costumbre).

    El zip: Wpf_Abrir_guardar_RichTextBox_20190112_2355.zip (34.7 KB)

    MD5 Checksum: 461518743ED0913B642C788342774057

  • El contenido de un RichTextBox de WPF, leer como cadena y asignar uno nuevo

    Pues eso… sigo con las cosillas que estoy aprendiendo a hacer al meterme con esto de programar RichTextBox en WPF (Xaml) y ahora le toca el turno a leer el contenido de un RichTextBox y devolverlo como cadena y a lo contrario: teniendo una cadena, asignarla a un RichTextBox, básicamente ese texto a asignar será en formato de texto enriquecido (Rtf).

    El código que te voy a mostrar está adaptado de los que me he encontrado buscando en la web, adaptado porque están en C# y los he convertido a Visual Basic y porque les he añadido otras comprobaciones para que funcione como yo quiero

    Devolver como cadena el texto de un RichTextBox (WPF)

    Este código que lo he puesto en un método llamado getRtbText al que se pasa como argumento el control RichTextBox del que queremos extraer el contenido lo he adaptado de un ejemplo de la documentación en línea de Microsoft: Cómo: Extraer el contenido de texto de un control RichTextBox y básicamente lo que hace es definir un rango con el contenido completo del RichTextBox y devolverlo, muy simple, pero debes saber que existe una cosa llamada TextRange

    Por eso existen los foros, blogs y demás, para que nos iluminen con esas cosas que desconocemos.

    Aquí tienes el código para Visual Basic y C# tal como lo estoy usando en la aplicación de ejemplo para manipular y sincronizar el contenido de dos RichTextBox de WPF.

    ''' <summary>
    ''' Extrae el texto de un RichTextBox y lo devuelve como una cadena.
    ''' De un ejemplo en C# de:
    ''' https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
    '''     how-to-extract-the-text-content-from-a-richtextbox
    ''' </summary>
    Private Function getRtbText(ByVal rtb As RichTextBox) As String
        Dim textRange = New TextRange(rtb.Document.ContentStart,
                                      rtb.Document.ContentEnd)
        Return textRange.Text
    End Function
    
    /// <summary>
    ///  Extrae el texto de un RichTextBox y lo devuelve como una cadena.
    ///  De un ejemplo en C# de:
    ///  https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
    ///      how-to-extract-the-text-content-from-a-richtextbox
    ///  </summary>
    private string getRtbText(RichTextBox rtb)
    {
        var textRange = new TextRange(rtb.Document.ContentStart, 
                                      rtb.Document.ContentEnd);
        return textRange.Text;
    }
    

    Según la documentación, TextRange es una selección de contenido entre dos posiciones indicadas por TextPointers, y entre otras cosas, tiene una propiedad (Text) que devuelve el texto que hay entre esas dos posiciones.

    Simple, ¿verdad? Pues eso… Pero yo no tenía ni idea de que existía TextRange .

    Nota:
    El valor obtenido con esa función es el texto plano, es decir, sin formato, del contenido del control RichTextBox.

    Sigamos…

    Asignar el contenido de una cadena a un RichTextBox (WPF)

    Y esta es la segunda cosa que te quiero explicar hoy. Teniendo una cadena de texto enriquecido (con código interno de RTF o de otros formatos) asignarla a un RichTextBox.

    Hay un montón de formatos aceptados por el RichTextBox, los que están en la enumeración DataFormats, que son tanto de imágenes como de texto, pero aquí solo veremos tres:

    • Rtf el contenido a asignar debe estar en formato RTF.
    • Xaml el contenido a asignar debe estar en formato Xaml pero el que se puede poner en un RichTextBox o FlowDocument, en mis pruebas me ha admitido solo lo que está dentro de Section (ver el código de ejemplo). Es decir, no vale cualquier código Xaml.
    • Text Formato de texto plano, sin ningún tipo de formato.

    La función que se encarga de asignar el contenido del RichTextBox se llama setRtbText que recibe como argumentos el control RichTextBox al que queremos asignar el texto, el texto a asignar y como último argumento el formato, que debe ser uno de los indicados en la enumeración DataFormats o el hard-code correspondiente, por ejemplo, para Rtf es Rich Text Format, para Xaml es Xaml y para Text es Text.

    Nota:
    En el enlace de DataFormats en la documentación en línea, te indica los hard-codes que puedes usar.

    Te muestro el código de la función setRtbText y verás que el último parámetro es opcional y he puesto por defecto el valor de DataFormats.Rtf.

    ''' <summary>
    ''' Asigna el texto al RichTextBox
    ''' Adaptado de un código de C# de:
    ''' https://stackoverflow.com/questions/1367256/
    '''     set-rtf-text-into-wpf-richtextbox-control
    ''' </summary>
    Private Sub setRtbText(rtb As RichTextBox,
                           text As String,
                           Optional formato As String = "Rich Text Format")
        ' Antes usaba ASCII                                         (08/Ene/19)
        '   ASCIIEncoding.Default.GetBytes(text)
        Dim stream = New MemoryStream(Encoding.UTF8.GetBytes(text))
    
        ' Borrar el contenido del RichTextBox
        ' antes de añadir el nuevo contenido
        ' ya que me ha pasado que mezclaba el texto nuevo con 
        ' el que ya tenía
        rtb.Document.Blocks.Clear()
    
        ' Selection.Load(stream, DataFormats.Rtf)
        rtb.Selection.Load(stream, formato)
    
    End Sub
    
    /// <summary>
    ///  Asigna el texto al RichTextBox
    ///  Adaptado de un código de C# de:
    ///  https://stackoverflow.com/questions/1367256/
    ///     set-rtf-text-into-wpf-richtextbox-control
    ///  </summary>
    private void setRtbText(RichTextBox rtb, string text,
                            string formato = "Rich Text Format")
    {
        // Antes usaba ASCII                                         (08/Ene/19)
        var stream = new MemoryStream(Encoding.UTF8.GetBytes(text));
    
        // Borrar el contenido del RichTextBox
        // antes de añadir el nuevo contenido
        // ya que me ha pasado que mezclaba el texto nuevo con 
        // el que ya tenía
        rtb.Document.Blocks.Clear();
    
        rtb.Selection.Load(stream, formato);
    }
    

    Para llamar a este método podemos hacerlo indicando en el tercer argumento un valor de la enumeración DataFormats o la cadena correspondiente a su valor (hard-code string) y en este caso, como el tercer parámetro es opcional, si no lo indicamos usará el Rtf o la cadena correspondiente Rich Text Format.

    Escucho voces… a ver, a ver… Valeee… que quieres un ejemplo completo de cómo usar todo esto… valeeee ¡a sus órdenes!

    Un ejemplo práctico para usar todo lo explicado

    Y con un par de extras, ya que para que te resulte más fácil probar los tres formatos indicados, incluyo código de ejemplo RTF y XAML (el de texto no tiene mucho misterio).

    Ese código o texto de ejemplo está hard-code-ado (ya que estamos con el palabro ese), es decir, como en el código lo asigno como cadena, las cadenas que contengan deben tener dobles comillas dobles para que el editor usando (en mi caso el de Visual Studio) no lo interprete de forma no-texto.

    El código Xaml con el diseño de la aplicación (ventana principal)

    Este código vale tanto para Visual Basic como para C#, lo único que hay que indicar es el nombre del espacio de nombres (namespace) de la aplicación que hayas creado para probarlo.

    <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"
    
             <!-- Aquí tienes que indicar el espacio de nombres 
                 (y quitar este comentario o te dará error) -->
            xmlns:local="clr-namespace:Wpf_Leer_y_asignar_contenido_de_RichTextBox_vb"
    
            mc:Ignorable="d"
            Title="Leer y asignar contenido de RichTextBox (VB)" 
            WindowStartupLocation="CenterScreen"
            Height="450" Width="800">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" MaxWidth="100"/>
                <ColumnDefinition />
                <ColumnDefinition />
                <ColumnDefinition Width="Auto" MaxWidth="250" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" MaxHeight="30"/>
                <RowDefinition />
                <RowDefinition />
                <RowDefinition Height="Auto" MaxHeight="30"/>
                <RowDefinition />
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Label Content="El texto: " 
                   Grid.Column="0" Grid.ColumnSpan="1" 
                   FontWeight="Bold"
                   HorizontalAlignment="Left" />
            <StackPanel Orientation="Horizontal"
                        Grid.Column="1" Grid.ColumnSpan="2"
                        HorizontalAlignment="Right">
                <Label Content="Pulsa para pegar texto en formatos diferentes"/>
                <Button x:Name="btnRtf" Click="BtnRtf_Click"
                        Content="Ejemplo RTF" Margin="4" />
                <Button x:Name="btnXaml" Click="BtnXaml_Click"
                        Content="Ejemplo Xaml" Margin="4" />
                <Button x:Name="btnText" Click="BtnText_Click"
                        Content="Ejemplo Text" Margin="4" />
            </StackPanel>
            <TextBox x:Name="txt" 
                     BorderThickness="2" BorderBrush="Blue"
                     AcceptsReturn="True"
                     AcceptsTab="True"
                     TextWrapping="Wrap"
                     VerticalScrollBarVisibility="Visible"
                     Grid.Column="0" Grid.Row="1" 
                     Grid.ColumnSpan="3" Grid.RowSpan="2" />
            <StackPanel Orientation="Vertical" Margin="4"
                        Grid.Row="1" Grid.Column="3" Grid.RowSpan="2">
                <Button x:Name="btnLeer" Click="BtnLeer_Click"
                        Grid.Column="3" 
                        Content=" Leer del RichTextBox " />
                <Separator Height="20" />
                <Button x:Name="btnAsignar" Click="BtnAsignar_Click"
                        Grid.Column="3" Grid.Row="2" 
                        Content=" Asignar al RichTextBox " />
                <Label Content="Opciones de asignar:" />
                <RadioButton x:Name="optRtf" Content="Formato RTF" IsChecked="True" />
                <RadioButton x:Name="optText" Content="Formato Text" />
                <RadioButton x:Name="optXaml" Content="Formato Xaml" />
            </StackPanel>
            <Label Grid.Row="3" Grid.ColumnSpan="3" 
                   Content="El RichTextBox: "
                   FontWeight="Bold"
                   HorizontalAlignment="Left" />
            <RichTextBox x:Name="rtb" 
                         VerticalScrollBarVisibility="Visible"
                         BorderThickness="4" BorderBrush="Green"
                         AcceptsTab="True" AcceptsReturn="True"
                         Grid.Column="0" Grid.Row="4" 
                         Grid.ColumnSpan="5" Grid.RowSpan="3">
                <FlowDocument />
            </RichTextBox>
        </Grid>
    </Window>
    

    El aspecto en tiempo de diseño de este código es el mostrado en la siguiente figura:

    Figura 1. La ventana de la aplicación en tiempo de diseño.

    Como puedes comprobar, tenemos un control RichTextBox en la parte inferior (con borde verde), un TextBox en la parte superior (con borde azul), tres botones (arriba del todo) para pegar texto de ejemplo en cada uno de los tres formatos creo que son los más comunes (para manejar textos) y en la parte de la derecha otros dos botones, uno para leer el contenido del texto y recuperarlo como una cadena normal (string) que se mostrará en el TextBox y otro botón con tres opciones del formato que vamos a asignar.

    A destacar (sobre el código XAML del diseño) es el uso de Grid con columnas y filas para indicar dónde irá cada control y dos StackPanel para agrupar los botones y las opciones y etiquetas.

    El StackPanel de los tres botones tienen la opción Orientation = Horizontal para que se coloquen horizontalmente (apilados de izquierda a derecha) y el otro con Orientation = Vertical para que se apilen de arriba a abajo.

    El código completo tanto para Visual Basic como para C#.

    En ese código están asignadas las cadenas de los tres ejemplos a usar según se pulse en uno de los tres botones de ejemplo.

    Visual Basic:

    '------------------------------------------------------------------------------
    ' Leer y asignar texto a un RichTextBox                             (10/Ene/19)
    ' Para el artículo:
    ' El contenido de un RichTextBox de WPF, leer como cadena y asignar uno nuevo
    '
    ' (c) Guillermo (elGuille) Som, 2019
    '------------------------------------------------------------------------------
    Option Strict On
    Option Infer On
    
    Imports System
    
    Imports System.IO
    Imports System.Text
    
    Class MainWindow
        ''' <summary>
        ''' Extrae el texto de un RichTextBox y lo devuelve como una cadena.
        ''' De un ejemplo en C# de:
        ''' https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
        '''     how-to-extract-the-text-content-from-a-richtextbox
        ''' </summary>
        Private Function getRtbText(ByVal rtb As RichTextBox) As String
            Dim textRange = New TextRange(rtb.Document.ContentStart,
                                      rtb.Document.ContentEnd)
            Return textRange.Text
        End Function
    
        ''' <summary>
        ''' Asigna el texto al RichTextBox
        ''' Adaptado de un código de C# de:
        ''' https://stackoverflow.com/questions/1367256/
        '''     set-rtf-text-into-wpf-richtextbox-control
        ''' </summary>
        Private Sub setRtbText(rtb As RichTextBox,
                               text As String,
                               Optional formato As String = "Rich Text Format")
            ' Antes usaba ASCII                                         (08/Ene/19)
            '   ASCIIEncoding.Default.GetBytes(text)
            Dim stream = New MemoryStream(Encoding.UTF8.GetBytes(text))
    
            ' Borrar el contenido del RichTextBox
            ' antes de añadir el nuevo contenido
            ' ya que me ha pasado que mezclaba el texto nuevo con 
            ' el que ya tenía
            rtb.Document.Blocks.Clear()
    
            ' Selection.Load(stream, DataFormats.Rtf)
            rtb.Selection.Load(stream, formato)
    
        End Sub
    
        Private Sub BtnLeer_Click(sender As Object, e As RoutedEventArgs)
            ' Lee el contenido del RichTextBox y lo asigna a la caja de texto
            Dim s = getRtbText(rtb)
            txt.Text = s
        End Sub
    
        Private Sub BtnAsignar_Click(sender As Object, e As RoutedEventArgs)
            If optRtf.IsChecked Then
                setRtbText(rtb, txt.Text, DataFormats.Rtf)
            ElseIf optXaml.IsChecked Then
                setRtbText(rtb, txt.Text, DataFormats.Xaml)
            ElseIf optText.IsChecked Then
                setRtbText(rtb, txt.Text, DataFormats.Text)
            End If
        End Sub
    
        Private Sub BtnRtf_Click(sender As Object, e As RoutedEventArgs)
            txt.Text =
            {\colortbl ;\red255\green0\blue0;\red0\green176\blue80;}
    \ 
    Hola Mundo del texto enriquecido.\
    
    
            Esto es un \b fichero\b0  RTF.\
    
    }"
            optRtf.IsChecked = True
        End Sub
    
        Private Sub BtnXaml_Click(sender As Object, e As RoutedEventArgs)
            txt.Text =
    "<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""> 
        <Paragraph>
          <Run>Paragraph 1</Run>
        </Paragraph>
    
      <Paragraph>
        Before the LineBreak in Paragraph.
        <LineBreak />
        After the LineBreak in Paragraph.
        <LineBreak/><LineBreak/>
        After two LineBreaks in Paragraph.
      </Paragraph>
      <Paragraph>
        <Span Background=""Red"" Foreground=""White""><Bold>Fondo rojo</Bold></Span>
        <LineBreak/>
        Texto normal, <Bold>en negrita</Bold>, <Italic>en itálica</Italic>
        más texto normal... <Span Foreground=""Red""><Bold>¡Atención!</Bold></Span>
      </Paragraph>
    <Paragraph FontFamily=""Consolas"" 
               FontSize=""24"" FontWeight=""Bold""
               Foreground=""Green"">Párrafo con 
    <Span Foreground=""Blue"">varios</Span> atributos de 
    <Span Foreground=""FireBrick"">fuente</Span>.
    </Paragraph>
    <Paragraph FontSize=""28"" FontFamily=""Palatino Linotype""
      Typography.NumeralStyle=""OldStyle""
      Typography.Fraction=""Stacked""
      Typography.Variants=""Inferior"">
    Ahora va de números... <LineBreak/>
      <Run>
        0123456789 10 11 12 13
      </Run>
      <LineBreak/><LineBreak/>
      <Run>
        1/2 2/3 3/4
      </Run>
    </Paragraph>
    </Section>
    "
            optXaml.IsChecked = True
        End Sub
    
        Private Sub BtnText_Click(sender As Object, e As RoutedEventArgs)
            txt.Text =
    "Érase una vez un texto normal, que está en el TextBox para pasarlo al 
    RichTextBox, (si pulsas en el botón de asignar."
            optText.IsChecked = True
        End Sub
    End Class
    

    C#:

    // ------------------------------------------------------------------------------
    // Leer y asignar texto a un RichTextBox                             (10/Ene/19)
    // Para el artículo:
    // El contenido de un RichTextBox de WPF, leer como cadena y asignar uno nuevo
    // 
    // (c) Guillermo (elGuille) Som, 2019
    // ------------------------------------------------------------------------------
    
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace Wpf_Leer_y_asignar_contenido_de_RichTextBox_cs
    {
        /// <summary>
        /// Lógica de interacción para MainWindow.xaml
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
            }
    
            /// <summary>
            ///  Extrae el texto de un RichTextBox y lo devuelve como una cadena.
            ///  De un ejemplo en C# de:
            ///  https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
            ///      how-to-extract-the-text-content-from-a-richtextbox
            ///  </summary>
            private string getRtbText(RichTextBox rtb)
            {
                var textRange = new TextRange(rtb.Document.ContentStart,
                                              rtb.Document.ContentEnd);
                return textRange.Text;
            }
    
            /// <summary>
            ///  Asigna el texto al RichTextBox
            ///  Adaptado de un código de C# de:
            ///  https://stackoverflow.com/questions/1367256/
            ///     set-rtf-text-into-wpf-richtextbox-control
            ///  </summary>
            private void setRtbText(RichTextBox rtb, string text,
                                    string formato = "Rich Text Format")
            {
                // Antes usaba ASCII                                         (08/Ene/19)
                var stream = new MemoryStream(Encoding.UTF8.GetBytes(text));
    
                // Borrar el contenido del RichTextBox
                // antes de añadir el nuevo contenido
                // ya que me ha pasado que mezclaba el texto nuevo con 
                // el que ya tenía
                rtb.Document.Blocks.Clear();
    
                rtb.Selection.Load(stream, formato);
            }
    
            private void BtnLeer_Click(object sender, RoutedEventArgs e)
            {
                // Lee el contenido del RichTextBox y lo asigna a la caja de texto
                var s = getRtbText(rtb);
                txt.Text = s;
            }
    
            private void BtnAsignar_Click(object sender, RoutedEventArgs e)
            {
                if (optRtf.IsChecked == true)
                    setRtbText(rtb, txt.Text, DataFormats.Rtf);
                else if (optXaml.IsChecked == true)
                    setRtbText(rtb, txt.Text, DataFormats.Xaml);
                else if (optText.IsChecked == true)
                    setRtbText(rtb, txt.Text, DataFormats.Text);
            }
    
            private void BtnRtf_Click(object sender, RoutedEventArgs e)
            {
            {\colortbl ;\red255\green0\blue0;\red0\green176\blue80;}
    \ 
    Hola Mundo del texto enriquecido.\
    
    
            Esto es un \b fichero\b0  RTF.\
    
    }";
                optRtf.IsChecked = true;
            }
    
            private void BtnXaml_Click(object sender, RoutedEventArgs e)
            {
                txt.Text = @"<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""> 
        <Paragraph>
          <Run>Paragraph 1</Run>
        </Paragraph>
    
      <Paragraph>
        Before the LineBreak in Paragraph.
        <LineBreak />
        After the LineBreak in Paragraph.
        <LineBreak/><LineBreak/>
        After two LineBreaks in Paragraph.
      </Paragraph>
      <Paragraph>
        <Span Background=""Red"" Foreground=""White""><Bold>Fondo rojo</Bold></Span>
        <LineBreak/>
        Texto normal, <Bold>en negrita</Bold>, <Italic>en itálica</Italic>
        más texto normal... <Span Foreground=""Red""><Bold>¡Atención!</Bold></Span>
      </Paragraph>
    <Paragraph FontFamily=""Consolas"" 
               FontSize=""24"" FontWeight=""Bold""
               Foreground=""Green"">Párrafo con 
    <Span Foreground=""Blue"">varios</Span> atributos de 
    <Span Foreground=""FireBrick"">fuente</Span>.
    </Paragraph>
    <Paragraph FontSize=""28"" FontFamily=""Palatino Linotype""
      Typography.NumeralStyle=""OldStyle""
      Typography.Fraction=""Stacked""
      Typography.Variants=""Inferior"">
    Ahora va de números... <LineBreak/>
      <Run>
        0123456789 10 11 12 13
      </Run>
      <LineBreak/><LineBreak/>
      <Run>
        1/2 2/3 3/4
      </Run>
    </Paragraph>
    </Section>
    ";
                optXaml.IsChecked = true;
            }
    
            private void BtnText_Click(object sender, RoutedEventArgs e)
            {
                txt.Text = @"Érase una vez un texto normal, que está en el TextBox para pasarlo al 
    RichTextBox, (si pulsas en el botón de asignar.";
                optText.IsChecked = true;
            }
    
        }
    }
    

    El código de ejemplo para Xaml interno

    A resaltar el texto a asignar para el formato XAML, para que veas que no es el código XAML de diseño de WPF, por ejemplo que te he mostrado antes, si no, el usado internamente por el control.

    Ese ejemplo lo podrías poner dentro de la definición de FlowDocument y quedaría de esta forma:

    <Section xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
        <Paragraph>
            <Run>Paragraph 1</Run>
        </Paragraph>
        <Paragraph>
            Before the LineBreak in Paragraph.
            <LineBreak />
            After the LineBreak in Paragraph.
            <LineBreak/>
            <LineBreak/>
            After two LineBreaks in Paragraph.
        </Paragraph>
        <Paragraph>
            <Span Background="Red" Foreground="White">
                <Bold>Fondo rojo</Bold>
            </Span>
            <LineBreak/>
            Texto normal,
            <Bold>en negrita</Bold> ,
            <Italic>en itálica</Italic>
            más texto normal...
            <Span Foreground="Red">
                <Bold>¡Atención!</Bold>
            </Span>
        </Paragraph>
        <Paragraph FontFamily="Consolas" 
                   FontSize="24" FontWeight="Bold"
                   Foreground="Green">Párrafo con
            <Span Foreground="Blue">varios</Span> atributos de
            <Span Foreground="FireBrick">fuente</Span> .
        </Paragraph>
        <Paragraph FontSize="28" FontFamily="Palatino Linotype"
                   Typography.NumeralStyle="OldStyle"
                   Typography.Fraction="Stacked"
                   Typography.Variants="Inferior">
            Ahora va de números...
            <LineBreak/>
            <Run>
                0123456789 10 11 12 13
            </Run>
            <LineBreak/>
            <LineBreak/>
            <Run>
                1/2 2/3 3/4
            </Run>
        </Paragraph>
    </Section>
    

    Fíjate en las asignaciones del párrafo último, para los números, que tiene definiciones tipográficas especiales. Ese ejemplo está sacado de la documentación en línea de Visual studio.

    Aquí tienes una captura de la aplicación (usando el código de C#) en tiempo de ejecución mostrando el texto con el formato Xaml.

    Figura 2. La aplicación en funcionamiento mostrando el código de ejemplo usado para el formato XAML.

    Y esto es todo por hoy, que para mí ya es casi mañana… le falta poco más de 15 minutos para que acabe el día 10

    Y para la próxima… cómo guardar y leer desde ficheros. Es decir, leer el contenido de un archivo y asignarlo al RichTextBox (usando los tres formatos usados aquí) y guardar el contenido de un RichTextBox en esos tres formatos, pero eso será… ma-ña-na…

    Espero que te sea de utilidad… ya sabes que esa es la idea

    Nos vemos.
    Guillermo

  • Sincronizar el scroll vertical y horizontal de varios TextBox

    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
    {
        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 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.

    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.

Salir de la versión móvil