Archivo de la etiqueta: xaml

Cómo refrescar los datos en aplicaciones de Xamarin.Forms o .NET MAUI

Pues eso… con el título de este post: Cómo refrescar los datos en aplicaciones de Xamarin.Forms o .NET MAUI, aparte de referirme que es para apliciones multiplataforma/multidispositivos creadas con Xamarin.Forms o .NET MAUI, me refiero a esos casos en los que quiero que se muestre un mensaje (por ejemplo en el texto de una etiqueta) mientras la aplicación hace cosas que pueden tardar un poco, como leer los datos de una base de datos o contar hasta cientocuincuentamilmillonesdetrillones, por decirte algo 😉

En .NET Framework / aplicaciones de Windows.Forms, yo suelo usar el fatídico (para algunos) Application.DoEvents(), que sí, que dicen que usando el Thread.Sleep se supone que hace lo mismo, pero yo te digo que no, a mí no me ha funcionado nunca.

Con la aplicación mobile en la que estoy enfrascado desde hace ya unos 3 meses, intenté el Sleep, intenté crear/usar un temporizador (otra de las formas en que hago con .NET Framework esa pequeña pausa para refrescar), pero nada de nada… Probé haciendo los métodos asíncronos para que fuesen «agüeitable» (async y await), pero nada de nada…

Sin embargo ayer, buscando en la red de redes (Inernet por si no sabías que así se le llama algunas veces), me encontré con una consulta en la que respondían eso que tanto estaba buscando saber cómo hacer, y la respuesta la encontré, (casi como de costumbre), en stackoverflow, concretamente aquí.

Y es usando Task.Delay(...) a la que tenemos que indicar los milisegundos de espera.
En esa respuesta indican dos formas de hacerlo: de forma asíncrona y de forma síncrona, decirte que la que a mí me ha funcionado es la asíncrona, la otra, como si miraras al cielo esperando ver pasar un elefante rosa… por decir algo… Vamos, que ni de coña esperaba…

Y como he visto que funcionaba, me he creado un método compartido en la clase App para que me sea fácil usarla desde cualquier parte de la aplicación, a ese método lo he llamdo Refrescar y recibe un parámetro con los milisegundos que quieres esperar antes de seguir, yo suelo usar el valor 300, que es el que casi siempre uso en los temporizadores que pongo en las aplicaciones de escritorio creadas con .NET Framework.
Este es el código de ese método (en C# ya que por ahora las aplicaciones de Xamarin.Forms / .NET MAUI son (y segurián siendo solo) para el cochambros lenguaje ese de los puntos y comas.

Y si te ha molestado que diga cochambroso lenguaje, de verdad que lo siento, pero… es lo que me parece a mí, al menos en la forma que yo lo uso, es una pequeña caca comparado con Visual Basic. Ya sabes: cochambros es lleno de mugre o cochambre que viene de cocho, puerco… ¡UF! me vas a odiar como seas fanático del C#, pero es lo que hay, es mi opinión, pero con fundamentos.

El código

Bueno, a lo que vamos, este es el código:

/// <summary>
/// Hacer una pequeña pausa para refrescar.
/// </summary>
/// <param name="intervalo">El tiempo que hay que esperar (en milisegundos).</param>
async public static Task Refrescar(int intervalo = 300)
{
    await Task.Delay(intervalo);
}

El valor predeterminado es de 300 milisegundos, es decir, menos de un tercio de segundo, incluso con 100 iría bien, pero…

Cómo lo utilizo

En uno de los casos, tengo una etiqueta en la que muestro el total de datos que leo de la base de datos, mientras se hace la búsqueda muestro un mensaje diciendo algo como: Leyendo los datos…, hago la pausa, (se refreca el contenido de la etiqueta) y mientras llamo al método que lee los datos, en el que actualizo esa etiqueta con los datos mostrados, etc.

LabelInfoReservas.Text = $"Leyendo las reservas de '{ElCliente.Nombre}'...";

// refrescar...
//await Task.Delay(300);
await App.Refrescar();

await asignarLasReservas();

En la siguiente captura, puedes verlo (me ha dado tiempo a hacerla a pesar de que había pocos datos que leer porque, esa es la impresión que a mí me da, las aplicaciones para Windows-UWP de Xamari.Forms / .NET MAUI son algo lentas… en ocasiones muuuucho más lentas que las aplicaciones para escritorio hechas con .NET Framework 4.8. Tanto que esta app móvil no podremos usarla al 100% en la playa porque hay poca cobertura móvil (de datos) que ya se hace insufrible utilizarla, y en este caso no creo que sea porque está hecha con C# 😉

Captura de la app con el mensaje mientras lee los datos de la base de datos.

Y ya está… eso es todo, simple, ¿verdad? pues… sí… 😉

Nos vemos.
Guillermo

Acceder a los recursos definidos en ResourceDictionary o Application.Resources desde código (C#) (Xamarin.Forms y NET MAUI)

Pues eso… es alqo que me ha estado dando un poco la lata en las aplicaciones de Xamarin.Forms, y es acceder a los recursos definidos en el fichero App.xaml tanto dentro de Application.Resources como en las definiciones de ResourceDictionary.

Aquí te explico cómo hacerlo en los dos casos, además de usar un fichero externo con las definiciones de los recursos.

 

Contenido:

 

Empezaré con las definiciones directas en los recursos de App.xaml.

Acceder desde código a las definiciones de recursos definidos en App.xaml

 —

Acceder desde código a las definiciones de Application.Resources

Para acceder a los recursos definidos en Application.Resources desde código es simple (al menos cuando se sabe cómo 😉 ).
Por ejemplo, si tenemos dos recursos definidos, uno ColorAzul1 y el otro ColorAzul2, de esta forma:

<Application.Resources>
    <Color x:Key="ColorAzul">#0073cf</Color>
    <Color x:Key="ColorRojo">Firebrick</Color>
</Application.Resources>

Si queremos usar uno u otro desde código dependiendo de alguna condición, por ejemplo que estemos usando la app en Android o en iOS y que ocurra alguna otra condición (ver nota), podemos hacerlo de esta forma:

if (esUnaPrueba)
    LabelInternet.TextColor = (Color)Application.Current.Resources["ColorAzul"];
else
    LabelInternet.TextColor = (Color)Application.Current.Resources["ColorRojo"];

Como ves en el código a los recursos definidos en Application.Resources se accede usando Application.Current.Resources["key del recurso"];

El cast usando en el código de ejemplo es necesario ya que el valor devuelto por ese elemento de la colección Resources es de tipo object.
Esa forma de hacerlo la conocí gracias a esta entrada en los foros de Xamarin.

Nota: Lo de que ocurra alguna otra condición es porque en los recursos podemos indicar para qué plataforma lo definimos. Aunque para el caso de los colores no sé muy bien como se haría, para el resto puedes ver esto.

 

Acceder desde código a las definiciones de ResourceDictionary

Por otro lado, si el recurso lo tenemos en una rama de ResourceDictionary, por ejemplo de esta forma:

    <ResourceDictionary>
        <Color x:Key="Color2Azul">#0073cf</Color>
        <Color x:Key="Color2Rojo">Firebrick</Color>
    </ResourceDictionary>
</Application.Resources>

Para acceder a esos recursos definidos en ResourceDictionary desde el código de C# lo tendrás que hacer de esta otro forma:

if (esAzul)
    LabelInternet2.SetDynamicResource(Label.TextColorProperty, "Color2Azul");
else
    LabelInternet2.SetDynamicResource(Label.TextColorProperty, "Color2Rojo");

El truco para usar esa otro forma, lo saqueé del mismo post que te indiqué antes, pero estaba abajo del todo, concretamente de aquí.
A lo mejor hay otra forma, pero… a mí me ha servido esta que te muestro.

 

Ahora toca explicarte cómo agregar ficheros de recursos (ResourceDictionary) y usarlos directamente en los recursos del fichero App.Xaml.

Definir recursos (ResourceDictionary) en fichero separado y usarlo en App.xaml

También puedes usar esos ficheros de recursos desde una página concreta.

Ahora también te lo explicaré, que a mí me da mucho coraje cuando se comenta algo así en un post y después no se explica porque se supone que se sabe cómo hacerlo… y aunque puede que sepas cómo hacerlo (eso hasta yo lo sabía hace unos cuantos años cuando empecé con todo esto de XAML, hará al menos 16 años), puede que hayas encontrado este post al buscar cómo hacer eso concretamente y no el resto de cosas que estoy explicando.

Crear un fichero XAML de recursos del tipo ResourceDictionary

En el proyecto añade un nuevo fichero del tipo XAML, yo no he visto cómo agregar uno directamente desde la ventana de agregar nuevo elemento al proyecto, así que, puedes agregar un fichero de texto y después cambiarle la extensión de .txt a .xaml 😉
En la figura 1 puedes ver más claro cómo agregar ese fichero.

Figura 1. Añadir un nuevo fichero al proyecto.

Nota: Después de agregar el fichero y cambiar la extensión (ya sea a la hora de crearlo como es el ejemplo de la figura 1 o si le cambias posteriormente la extensión, es recomendable cerrarlo y después abrirlo, si no, el Intellisense no se cosca de que es del tipo que tiene la extensión. Esto también es válido para ficheros de código al que inicialmente lo has creado como .TXT y después lo cambias a .CS.

Ese fichero estará vacío, así que… habrá que ponerle algo de código, concretamente este que te muestro para que sea un fichero válido del tipo ResourceDictionary.

<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">

</ResourceDictionary>

Aquí voy a definir algunas cosas para ver cómo funciona, tanto en modo diseño (en el código XAML de las páginas) como desde el código (en estos ejemplos solo de C# que es lo que me permite usar loss proyectos de Xamarin.Forms.
Aunque lo mismo hago pruebas con aplicaciones del tipo UWP que ahí sí se pueden crear proyetos de C# o de mi querido Visual Basic 😉

En ese fichero de recursos agrega las siguientes definiciones de colores y estilos de etiquetas para que quede como te muestro a continuación.

<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">

    <Color x:Key="Color3Azul">#0073cf</Color>
    <Color x:Key="Color3Rojo">Firebrick</Color>
    <Color x:Key="Color3Blanco">White</Color>

    <Style x:Key="LabelEjemplo3" TargetType="{x:Type Label}">
        <Setter Property="FontSize" Value="Small"/>
        <Setter Property="FontAttributes" Value="None" />
        <Setter Property="HorizontalTextAlignment" Value="Start"/>
        <Setter Property="HorizontalOptions" Value="FillAndExpand"/>
        <Setter Property="Padding" Value="4"/>
        <Setter Property="TextColor" Value="Green"/>
        <Setter Property="BackgroundColor" Value="{DynamicResource Key=Color3Blanco}"/>
    </Style>
    <Style x:Key="LabelEjemplo3Azul" TargetType="{x:Type Label}">
        <Setter Property="FontSize" Value="Small"/>
        <Setter Property="FontAttributes" Value="Italic" />
        <Setter Property="HorizontalTextAlignment" Value="Start"/>
        <Setter Property="HorizontalOptions" Value="FillAndExpand"/>
        <Setter Property="Padding" Value="4"/>
        <Setter Property="TextColor" Value="{StaticResource Color3Azul}"/>
        <Setter Property="BackgroundColor" Value="{StaticResource Color3Blanco}"/>
    </Style>
    <Style x:Key="LabelEjemplo3Rojo" TargetType="{x:Type Label}">
        <Setter Property="FontSize" Value="Small"/>
        <Setter Property="FontAttributes" Value="Bold" />
        <Setter Property="HorizontalTextAlignment" Value="Start"/>
        <Setter Property="HorizontalOptions" Value="FillAndExpand"/>
        <Setter Property="Padding" Value="4"/>
        <Setter Property="TextColor" Value="{StaticResource Color3Rojo}"/>
        <Setter Property="BackgroundColor" Value="{StaticResource Color3Blanco}"/>
    </Style>

</ResourceDictionary>

 

Usar un fichero de recursos (ResourceDictionary) desde una página

Y con eso puedes hacer lo siguiente para incluir el fichero de recursos en una página concreta.
Se supone que la página está en una carpeta (en mi caso en la carpeta elGuille) y el fichero de recursos está en el raíz del proyecto. Por tanto tengo que usar ../ para acceder al fichero que está en un path superior. Esto es como cuando accedes a los ficheros, así que no creo que tengas problemas para entenderlo… o eso espero, porque en esto sí que no voy a profundizar 😉

<ContentPage.Resources>
    <ResourceDictionary Source="../ResourceDictionaryElGuille.xaml"/>
</ContentPage.Resources>

Nota: Si estuviera en otra carpeta, tendrás que indicarlo en el path de la propiedad Source. Por ejemplo si la carpeta está en el raíz del proyecto y se llama Recursos, el valor de Source quedaría así: Source="/Recursos/ResourceDictionary2.xaml".

 

Usar por código un recurso (estilo) definido en ResourceDictionary.

Para usar, en este ejemplo, un estilo definido, habría que hacerlo de esta forma:

if (usarEstiloAzul)
    LabelInternet3.SetDynamicResource(Label.StyleProperty, "LabelEjemplo3Azul");
else
    LabelInternet3.SetDynamicResource(Label.StyleProperty, "LabelEjemplo3Rojo");

Es decir, como en el caso de los colores, pero en lugar de usar TextColorProperty, es decir, una propiedad, uso StyleProperty para indicar que es un estilo lo que quiero aplicar desde el fichero de recursos.

 

Por último voy a explicarte cómo agregar ese fichero de recursos (ResourceDictionary) al fichero App.xaml y así poder usarlo en toda la aplicación.

Usar un fichero de recursos (ResourceDictionary) en App.xaml

Seguramente esto parece no tener mucho sentido, ya que si defines los recursos directamente en el fichero App.xaml ¿por qué hacerlo en otro fichero?

En mi caso, la respuesta es la siguiente:
Yo tengo un proyecto de Xamarin Forms que define las clases a usar en la aplicación móvil (Android, iOS y UWP), llamaré Proyecto1 a ese proyecto base (el que se usa desde los proyectos específicos de cada plataforma).

Ahora creo otro proyecto móvil (que llamaré Proyecto2) que tiene también como punto de entrada el fichero App (es lo habitual) y que usa la DLL del proyecto anterior (Proyecto1). Lo hago así, porque la funcionalidad es la misma, pero por circunstacias que no viene al caso profundizar mucho en ellas, tuve que crear otro proyecto móvil (el que usa lo definido en Proyecto2), pero para no repetir todo el código que ya tengo hecho, añadí una referencia a Proyecto1 en el Proyecto2.

Y lo curioso es que aunque las clases definidas en Proyecto1 se supone que utiliza los recursos que haya en el App.xaml del Proyecto1, al ejecutar la aplicación (que usa Proyecto2) me da error de que no encuentra los recursos, por tanto, tuve que copiar todas las definiciones del fichero App.xaml del Proyecto1 en el App.xaml del Proyecto2.

Hasta aquí todo bien, sin problemas. ¿No?

Pero ahora decido añadir un nuevo estilo o cambiar alguno de los que hay… ¿Qué tengo que hacer? Pues muy fácil: MODIFICAR ambos ficheros App.xaml con esos estilos modificados.

Y la verdad es que para un despistao como yo, eso es unproblema, ya que me puedo tirar un buen rato comprobando porqué no funciona con los nuevos estilos definidos… Hasta caer en la cuenta de que no están definidos en los dos ficheros.
Sí, así de torpe es elGuille 😉

¿Solución? Crear un fichero en el que estén todos los recursos y ese fichero usarlo en los dos proyectos. Y eso es lo que he hecho y como he visto que en la rede de redes no está explicado cómo hacer esto, seguramente porque es algo trivial o fácil, pero la cuestión es que para mí no era tan trivial ni fácil, y como no encontraba la solución… me puse a probar con lo que ya había encontrado y… ahora ya lo tengo claro y para que tú también lo tengas claro, si es que necesitas hacerlo, aquí te lo explico.

En realidad la solución es fácil, lo que me traía de cabeza es que usando esas definiciones con esto: Application.Current.Resources["estilo"], no me funciona (y eso aún no lo he solucionado), así que, tuve que usar lo que te he comentado antes de aplicar los estilos que están en una declaración ResourceDictionary (Acceder desde código a las definiciones de ResourceDictionary), es decir, usando esto: Control.SetDynamicResource(TipoControl.StyleProperty, "estilo"), ya que al fin y al cabo, esos recursos definidos en otro fichero en realidad están definidos como ResourceDictionary.

Vamos con los ejemplos de código, empezando por el código XAML a usar en App.xaml:

<Application.Resources>
    <ResourceDictionary Source="ResourceDictionaryElGuille.xaml"/>

Es decir, se hace de la misma forma que para añadirlo a una página normal.
En este caso no indico ningún path en Source porque ese fichero de recursos está en el mimo directorio que App.xaml.

Si ves el código fuente en GitHub (es el mismo proyecto que los ejemplos de los Trucos para Xamarin.Forms y .NET MAUI en dispositivos) comprobarás que en App.xaml he dejado ese código anterior (el de usar un ResourceDictionary definido en otro fichero) además de otras definiciones que también están en bloques ResourceDictionary.

 

A tener en cuenta

Lo que sí debes tener en cuenta es que si todo lo defines en bloques de ResourceDictionary no podrás acceder a esas definiciones por código usando Application.Current.Resources[«key»].

Es decir, el código usado en el primer ejemplo fallará si esos dos recursos (ColorAzul y ColorRojo) no están directamente definidos en los recursos de la aplicación.

Pero puedes tenerlos definidos en dos sitios diferentes sin conflictos.

Para que todos estos ejemplos funcionen he definido esos dos valores tanto en Application.Resources como en el ResourceDictionary del fichero ese externo.

Por tanto, el código XAML de App.xaml empezaría de esta forma: (sigue con otras definiciones de ResourceDictionary, una que estaba ya en ese fichero cuando se creé el proyecto y los que he definido para usar los colores Color2Azul y Color2Rojo del segundo ejemplo que te mostré antes).
También he definido los recursos ColorAzul y ColorRojo para que el primer ejemplo no de error. Y sí, esas mismas claves están definidas en ambos ficheros, y sin conflictos. 😉

<Application.Resources>
    <ResourceDictionary Source="ResourceDictionaryElGuille.xaml"/>

    <ResourceDictionary>
        <Color x:Key="Color2Azul">#0073cf</Color>
        <Color x:Key="Color2Rojo">Firebrick</Color>
    </ResourceDictionary>

Como ya te he explicado antes, la forma de usar esos valores definidos en un ResourceDictionary es usando el código del segundo ejemplo de este post.

 

Y esto es todo… espero que te haya sido de utilidad.

El código fuente de estos ejemplos (y los del 2 de junio) están en GitHub: ElGuille-Ejemplos.

 

Nos vemos.
Guillermo

Instalando los proyectos de VS2019 v16.11 Preview 1 para .NET MAUI Preview 4

Pues eso… que he instalado el Visual Studio 2019 versión 16.11 Preview 1 para poder usar los proyectos de .NET MAUI Preview 4 y como no había tipos de proyectos para .NET MAUI, y lo que se indica en esa página para tenerlos no es completo, he tenido que buscar en la WEB cómo hacerlo y aquí te pongo unas capturas de ese proceso de configuración de .NET MAUI para poder tener los tipos de proyectos en Visual Studio 2019 v11 Preview 1.

Lo primero es decirte que al PowerShell deberían cambiarle las combinaciones de colores, porque lo que es un servidor, no me resulta fácil ver algunos de los avisos y comandos/opciones… así que… si tampoco puedes verlos en las capturas… imagina lo que pone… 😀

De todas formas te resumo los pasos:

Tienes que abrir una ventana de comandos o PowerShell y escribir:
dotnet tool install -g redth.net.maui.check
A continuación (esto es lo que no vi en la página del anuncio de .NET MAUI Preview 4, aunque ahora releyéndola si lo he visto) escribir:
maui-check
Aunque antes de esto deberías escibir estos dos comandos:
primero:
dotnet new nugetconfig
y después:
dotnet nuget add source -n maui-preview https://aka.ms/maui-preview/index.json
O al menos así lo hice yo, aunque creo que primero escribí el primero el de dotnet tool install
En cualquier caso, si ya lo has hecho, no habrá problemas, tal como verás en las siguientes capturas, ya que te dirá que ya está hecho 😉

Figura 1. No me muestran los proyectos de .NET MAUI.
Figura 2. Ahí dice que ya lo he instalado… pero yo no veo ese texto… tuve que copiarlo para verlo…
Figura 3. Crear la configuración de NuGet y añadirla… aunque ya lo hice según ahí me indica
Figura 4. La comprobación de si está instalado lo necesario para usar .NET MAUI (tuve que «fix» todo eso…).
Figura 5. Y paciencia mientras instala los SDKs, emuladores, etc.
Figura 6. Paciencia mientras sigue instalando…
Figura 7. Hasta que termine de comprobar e instalar.
Figura 8. Ahora si hay proyectos para .NET MAUI en Visual Studio 2019 v11 Preview 1

Por supuesto, estos pasos no habrá que hacerlos una vez que tanto el .NET MAUI como el Visual Studio 2019 (o 2022) que lo soporte estén más actualizados. O eso espero, si no… pues eso… ¡será un rollo!

Y si quieres saber qué se irá incluyendo en las próximas preview de .NET MAUI, aquí tienes el roadmap mostrado cuando sacaron la preview 4.

No he tenido oportunidad de probarlo, he creado un proyecto nuevo, pero la máquina virtual que tengo parece que falla mucho o es que el Visual Studio 2019 versión 16.11 Preview 1 hace que falle, no lo sé… no tengo la paciencia para averiguarlo, entre otras cosas porque esa máquina virtual (en Hyper-V) va lenta y me ralentiza mi equipo, al menos con las cosas que tengo abiertas y que por ahora son más urgentes que probar el .NET MAUI, pero lo probaré… y cuando lo haga, lo sabrás… si sigues lo que publico aquí en el blog o en mi feis, así que… te invito a que entres en la página de elGuille ne el feis: elGuille.info en Facebook.

Espero que te sea de utilidad… ya sabes que esa es siempre la intención. 😉

Nos vemos.
Guillermo

Soluciones a los ejercicios de Trucos para Xamarin.Forms y .NET MAUI en dispositivos

Pues eso… aquí tienes los ejercicios y las soluciones propuestos en el post Trucos para Xamarin.Forms y .NET MAUI en dispositivos. También los tienes disponibles en GitHub.

Los ejercicios eran:

Si quieres, como ejercicio de práctica, puedes hacer lo siguiente:

Ejercicio 1:

Añade un panel (StackLayout) con la orientación horizontal, de esa forma los controles que metas dentro de ese stack se mostrarán uno al lado del otro, pon un Switch seguido de un botón con el texto «Un Switch» y después otro Switch y otro botón con el texto «Otro Switch» (o lo que te de la gana pero que no sea demasiado largo).
En este caso el texto no pillará todo el ancho y verás que no se ve igual en iOS / iPhone que en Android o UWP/Windows.
Si no puedes probarlos en distintos dispositivos emuladores, te pondré la respuesta con algunas capturas de los tres sistemas que utilizo con Xamarin.Forms.

Nota:
Probando el ejercicio, compruebo que el poner más espacios al final, no tienen el efecto deseado, así que… si es iOS usa el valor 6,0 para la propiedad Padding.

Otra cosa interesante que puedes hacer (ejercicio 2) es que las etiquetas estén alineadas en el centro vertical, es decir, para que no se vean con el texto tan arriba, sino en el centro de la propia etiqueta.

Estas son las soluciones

Al ejercicio 1 y 2:

<StackLayout Orientation="Horizontal" Padding="4" Spacing="3">
    <Label Text="2 Switch con botones: " VerticalOptions="Center"/>
    <Switch />
    <!-- Los espacios detrás no se respetan... habrá que usar padding -->
    <Button Text=" Un Switch "
            Padding="{OnPlatform iOS='6,0', Android='6,0', Default=4}"/>
    <Switch />
    <Button Text=" Otro Switch "
            Padding="{OnPlatform iOS='6,0', Android='6,0', Default=4}"/>
</StackLayout>

Nota:
El valor ColorAzul2 está definido en App.xaml.

En ej ejercicio 2 lo que debes usar es: VerticalOptions="Center"

Capturas de los ejemplos

Aquí te dejo algunas capturas (me gusta que los posts tengan imágenes 😉 )

Ya sabes, pulsa en la imagen para verla en grande.

Figura 1. Página principal del proyecto en Android.

Figura 2. La solución a los ejercicios en Android.

Figura 3. Página principal del proyecto en iPhone.

Figura 4. Las soluciones en iPhone, pero en modo oscuro no se ve la etiqueta…

Figura 5. Solución a los ejercicios en iPhone (usando el tema claro).

Y esto es todo.

Lee el post de estos ejercicios porque hay cambios.

Nos vemos.
Guillermo

Trucos para Xamarin.Forms y .NET MAUI en dispositivos

Pues eso… te voy a relacionar varios trucos sobre cómo usar el código (y el código de marcado XAML) para dispositivos móviles (Android, iOS y UWP -Windows 10-) usando Xamarin.Forms (Mono mobile) y seguramente servirá para .NET MAUI, la que será el sustituto de Xamarin cuando .NET 6.0 esté en fase release (sobre noviembre de este año 2021).

Nota:
El código mostrado a la hora de escribir este post (2 de junio de 2021 a las 11:15) es para Xamarin.Forms y C#.
Cuando tenga ejemplos de .NET MAUI lo indicaré si es diferente al de Xamarin.

Los trucos

El código con los ejemplos, ejercicios y soluciones están publicados en GitHub.

  1. Saber la plataforma (Android, iOS o UWP) y el tipo de dispositivo (Phone, Tablet, TV, Watch, Desktop)
  2. Poner varios valores en el código XAML usando OnPlatform, OnIdiom, etc.
    1. Ejercicio 1
    2. Ejercicio 2
  3. Si al crear un proyecto no te aparecen los emuladores ni dispositivos
  4. Acceder a los recursos definidos en ResourceDictionary o Application.Resources desde código (C#) (en otro post del 14-jun-2021)

Saber la plataforma (Android, iOS o UWP) y el tipo de dispositivo (Phone, Tablet, TV, Watch, Desktop)

Algunas veces es conveniente saber si la aplicación está funcionando en un dispositivo Android, iOS o compatible con UWP (Windows 10 escritorio o móvil, Xbox, etc.), tanbién si es un móvil, escritorio, tablet, etc.

La primera (la plataforma) se averigua con Platform y OnPlatform y los valores aceptados son los indicados en la tabla 1 (de la ayuda de Xamarin.Forms) que corresponden al tipo DevicePlatform:

La segunda (el tipo de dispositivo) se averigua con Idiom y OnIdiom y los valores de DeviceIdiom que son los que te muestro en la tabla 2.

Cuando usamos Platform o Idiom lo haremos por código usando la clase DeviceInfo (definida en Xamarin.Essentials).
OnPlatform y OnIdiom lo usaremos en el código XAML.
Ahora veremos ejemplos de código c# y XAML de estos dos casos.

AndroidGets the Android platform.
iOSGets the iOS platform.
macOSMac OS
TizenGets the Tizen platform
tvOSGets the tvOS platform
UnknownGets the unknown platform.
UWPGets the UWP platform.
watchOSGets the watchOS platform
Tabla 1. Valores de la estructura DevicePlatform.

DesktopGets the desktop idiom.
PhoneGets the phone idiom.
TabletGets the tablet idiom.
TVGets the TV idiom.
UnknownGets the unknown idiom.
WatchGets the watch idiom.
Tabla 2. Valores de la estructura DeviceIdiom.

Código de ejemplo

Por código podemos usar DeviceInfo (habrá que importar el espacio de nombres Xamarin.Essentials) y las estructuras DevicePatform o DeviceIdiom según lo que queramos averiguar.

Nota:
En estos ejemplos estoy usando una página sencilla de tipo ContentPage de Xamarin.
En la que estarán definidos los controles usados en el código.

Por ejemplo, si queremos saber si el código está ejecutándose en un dispositivo UWP (por ejemplo, en Windows 10) o en Andoid o en iOS podemos hacer lo siguiente comprobando la propiedad Platform del tipo DeviceInfo:

// Saber la plataforma
if (DeviceInfo.Platform == DevicePlatform.UWP)
    LabelDevicePlatform.Text = "Estás usando la plataforma UWP";
else if (DeviceInfo.Platform == DevicePlatform.Android)
    LabelDevicePlatform.Text = "Estás usando la plataforma Android";
else if (DeviceInfo.Platform == DevicePlatform.iOS)
    LabelDevicePlatform.Text = "Estás usando la plataforma iOS";

Lo mismo te interesa saber si se está ejecutando en un teléfono móvil una tableta o en el escritorio, para eso usaremos la propiedad Idiom de la clase DeviceInfo:

// Saber el tipo de dispositivo
if (DeviceInfo.Idiom == DeviceIdiom.Desktop)
    LabelDeviceIdiom.Text = "Estás usando el escitorio.";
else if (DeviceInfo.Idiom == DeviceIdiom.Phone)
    LabelDeviceIdiom.Text = "Estás usando un teléfono móvil.";
else if (DeviceInfo.Idiom == DeviceIdiom.Tablet)
    LabelDeviceIdiom.Text = "Estás usando una tableta.";

Pero puede ser que lo que queremos es asignar un color según el código esté funcionando en una de las plataformas. En lugar de hacerlo por código, lo podemos hacer definiendo esa característica en el diseñador XAML.

Vamos a verlo aplicando un color al fondo de la etiqueta LabelDevicePlatform definida en el ejemplo.

<Label x:Name="LabelDevicePlatform" Padding="6"
       TextColor="White"
       BackgroundColor="{OnPlatform Android=#FA3F7E, iOS=#34C759, UWP=#0077DF}" />

En el código XAML es donde usaremos OnPlatform.

De igual forma podemos usar OnIdiom para saber el tipo de dispositivo.

<Label x:Name="LabelDeviceIdiom" Padding="6" 
       TextColor="{OnPlatform Android=Black, iOS=Blue, UWP=Green}"
       BackgroundColor="Wheat"
       FontSize="{OnIdiom Tablet=Small, Phone=Micro, Desktop=Medium}"/>

En ese código XAML hacemos las dos comprobaciones, la plataforma y el dispositivo y según sea aplicaremos un color u otro.

Si quieres hacer solo esa comprobación, por ejemplo para que en dispositivos de Apple (iOS) tengan un color y que en el resto tenga otro, podemos hacerlo de esta forma:

<Label Text="DeviceIdiom: " TextColor="{OnPlatform iOS=Green, Default=Red}"/>

En ese caso usamos el valor (o propiedad) Default que se aplicará a las plataformas (o dispositivos si se usa con OnIdiom) para las que no haya un valor concreto.

Poner varios valores en el código XAML usando OnPlatform, OnIdiom, etc.

Otra de las cosas que me ha pasado mientras estoy con esto de crear aplicaciones para dispositivos móviles y concretamente con el iPhone (iOS), en el que el texto mostrado en uno de los tipos de botones que estoy usando (para simular un Switch) es que el texto se queda pegado a los bordes del botón, mientras que en Android y UWP se quedaba separado.

Para solucionarlo necesitaba hacer dos cosas:
1- La más simple es poder añadir espacios delante y detrás del texto. Esto sería fácil si esa asignación la hago por código (c#), pero yo quería hacerla en el código de diseño (XAML).
2- Cambiar el Padding izquierdo y derecho.

El problema es que para el primer caso, tenía que asignar espacios delante y detrás del texto, pero ni usando el valor 255 (ALT+255) lo conseguí, ya que al ejecutar el código esos espacios se ignoran .

En el segundo caso, es que al asignar valores separados por comas da error, ya que después de cada coma se espera una propiedad.

Por ejemplo, lo del espacio.
Fíjate en el siguiente código XAML el valor del texto (propiedad Text) lo asigno usando un valor diferente según la plataforma.
Si es iOS uso un espacio delante y detrás de la palabra ATRÁS, en el resto de plataformas (Default) use ese mismo texto pero sin espacios:

<Button x:Name="btnAtras" 
        Text="{OnPlatform iOS= ATRÁS , Default=ATRÁS}" 
        HorizontalOptions="FillAndExpand"
        Style="{StaticResource BtnNavegar}"
        Clicked="btnAtras_Clicked"/>

Y si ese texto llevase comas, pues… ni se podría poner.

Ese sería el caso para cuando queremos asignar un valor que tenga varios argumentos separados por comas, como es el caso de Padding si no queremos asignar un solo valor para los 4 costados. Ya que solo me interesaba para la parte iaquierda y derecha, que si lo asignase de forma normal sería algo así:

Padding="6,0"

El primer valor se asignará a la izquierda y derecha (Left y Right) y el segundo valor arriba y abajo (Top y Botton).
Que queremos asignar 4 valores diferentes, lo haremos así:

Margin="4,5,6,7"

Los valores serían: 4 para la izquierda, 5 para arriba, 6 para la derecha y 7 para abajo.

La solución: Poner el texto entre comillas simples (las dobles no se pueden usar, ni siquiera usando teclas de ESCAPE, algo así: \»).

Por cierto, esa solución también es válida para el código HTML, cuando quieres poner comillas dentro de unas comillas, por ejemplo un valor style en el texto asignado a algo mediante código. Pero esa es otra historia.

Dicho lo dicho, la cosa quedaría de esta forma, si lo que se busca es que el padding izquierdo y derecho sea 6 para la plataforma iOS y 0 para el resto y que el texto incluya comas o espacios delante y/o detrás, si esos espacios están entro otros caracteres no hay que hacer nada especial.

<Label x:Name="LabelStatus" Style="{StaticResource LabelStatusWiz}"
       Padding="{OnPlatform iOS='6,0', Default=0}"
       Text="{OnPlatform Android=' Dispositivo \'Android\' ', 
                         iOS=' Dispositivo iPhone (\'iOS\') ', 
                         UWP='Dispositivo UWP (\'Windows 10, XBox\')'}"/>

Como ves el valor del Padding para iOS es ‘6,0’ (entre comillas simples).
Fíjate también que en ese código (para el valor de la propiedad Text) he usado \’ para indicar que quiero usar una comilla simple, esto solo funcionará si el texto completo está encerrado entre comillas simples.

Aquí tienes unas capturas de UWP: Windows 10 en el equipo local o lo que es lo mismo, usando el escritorio (figura 1), para Android (figura 2) y para iOS en un iPhone 7 Plus (figuras 3 y 4).

Observa los colores de las etiquetas (fondo y texto), así como en la parte de abajo que hay texto entre comillas simples.

Y observa que el padding para el botón ese con el texto tan largo, pues como que no sirve de mucho, ya que el botón pilla todo el ancho de la pantalla, pero sería útil si ese botón se está mostrando en un sitio que no lo expanda.

Si quieres, como ejercicio de práctica, puedes hacer lo siguiente:

Ejercicio 1:

Añade un panel (StackLayout) con la orientación horizontal, de esa forma los controles que metas dentro de ese stack se mostrarán uno al lado del otro, pon un Switch seguido de un botón con el texto «Un Switch» y después otro Switch y otro botón con el texto «Otro Switch» (o lo que te de la gana pero que no sea demasiado largo).
En este caso el texto no pillará todo el ancho y verás que no se ve igual en iOS / iPhone que en Android o UWP/Windows.
Si no puedes probarlos en distintos dispositivos emuladores, te pondré la respuesta con algunas capturas de los tres sistemas que utilizo con Xamarin.Forms.

Nota:
Probando el ejercicio, compruebo que el poner más espacios al final, no tienen el efecto deseado, así que… si es iOS usa el valor 6,0 para la propiedad Padding.

Otra cosa interesante que puedes hacer (ejercicio 2) es que las etiquetas estén alineadas en el centro vertical, es decir, para que no se vean con el texto tan arriba, sino en el centro de la propia etiqueta.

Pulsa en las imágenes para verlas a tamaño real.

Figura 1. El ejemplo en un Windows 10 (UWP).

Figura 2. El ejemplo en un móvil de Android (Pixel 4a).

Figura 3. El ejemplo en un móvil iOS (iPhone 7 Plus). Observa que no se ve todo el texto.

Figura 4. El ejemplo en un móvil iOS (iPhone 7 Plus).

Si miras las capturas 3 y 4 verás que en la última ya se ve bien el texto, tanto de la etiqueta de arriba como en la que tiene el fondo verde (la que indica que estás usando la plataforma iOS), para solucionar esa error, he vuelto a usar lo comentado en este post.

En el caso de la etiqueta de Xamarin he ajustado el valor de Padding para el iOS que sea más pequeño que para el resto.

<Label Text="Welcome to Xamarin.Forms!" FontSize="Title" 
       BackgroundColor="#96d1ff" Padding="{OnPlatform iOS=10,Default=40}"
       VerticalOptions="CenterAndExpand" 
       HorizontalOptions="CenterAndExpand" />

Para la etiqueta donde se muestra la plataforma en la que se está usando la aplicación, he optado por cambiar el tamaño de la letra.

<Label x:Name="LabelDevicePlatform" Padding="6"
       FontSize="{OnPlatform iOS=Micro, Default=Small}"
       TextColor="White"
       BackgroundColor="{OnPlatform Android=#FA3F7E, iOS=#34C759, UWP=#0077DF}" />

Nota importante sobre los valores asignados cuando usamos OnPlatform, OnIdiom, etc.
Cuando usamos, por ejemplo, OnPlatform el Intellisense de Visual Studio (en este ejemplo estoy usando Microsoft Visual Studio Community 2019 Version 16.10.0) te muestra los valores de la plataforma (iOS, Android, UWP, Default, etc.), pero no te muestra el intellisense para los valores asignados.
Por ejemplo, en el valor de FontSize del código XAML anterior, he usado el valor Micro para iOS y Small para el resto (Default), esos valores hay que ponerlos como se deben poner, me explico: si el valor es Micro con la m mayúscula, debe estar así escrito, si pones micro (con la m en minúscula) te dará error… Esto lo sé porque lo escribí en minúscula y al ejecutar el programa en el iPhone me dijo que nones… 😉

Si al crear un proyecto no te aparecen los emuladores ni dispositivos

Pues eso… he creado un nuevo proyecto para Xamarin.Forms para estos ejemplos y ejercicios y al intentar probarlo en otra plataforma distinta a UWP, vamos al intentar probarlo en Android o iPhone no me salían las opciones de los emuladores o los dispositivos.

¿La solución?
Cerrar la solución y volverla a abrir, así de simple.

En la página de las soluciones a los ejercicios tienes las capturas del proyecto que he creado y que está publicado en GitHub (aquí más abajo tienes el enlace).

Y esto es todo.

Solo falta publicar el proyecto en GitHub, pero como lo he usado en la aplicación que tengo para las reservas de MKN (la empresa donde trabajo), tendré que crear un proyecto/solución para poder ponerlo en GitHub y así poder descargarlo, etc.
Ya avisaré. Avisado estás de que ya está publicado en GitHub 😉
La solución a los dos ejercicios las pondré en otro post y también estarán en GitHub.

El código en GitHub

elGuille-Ejemplos

Nos vemos.
Guillermo

Indicar el Encoding al guardar el contenido de un RichTextBox de WPF

Pues eso… que el otro día te puse un ejemplo de Abrir y guardar archivos usando RichTextBox para WPF y anoche haciendo pruebas con vocales acentuadas, me di cuenta que el formato XAML (DataFormats.Xaml) las tildes se las pasaba por el forro… así que… buscando en la red de redes vi un ejemplo que evita eso… o casi, al menos te permite tener la opción de poder hacerlo.

El problema está (o estaba) en que en el ejemplo de donde saqué el código para guardarlo utiliza esto para crear el Stream de salida: Using fStream As New FileStream(_fileName, FileMode.Create) y usando FileStream no se puede indicar la codificación. O yo no sé cómo hacerlo, que todo hay que decirlo

Al abrir el Stream se hace la llamada al método Save del rango (TextRange) y se indica el formato con el que se guardará: range.Save(fStream, formato, True). range.Save precisa de un Stream, pero el ofrecido por StreamWriter, que es el que yo suelo usar para guardar indicando la codificación, no le sirve.

El truco está en guardar primero el contenido del RichTextBox en la memoria (usando MemoryStream) y después pasar ese flujo de caracteres al disco por medio de StreamWriter.

El código final quedaría de la siguiente forma:

''' <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)

    ' Para guardar con el formato que queramos
    ' Adaptado de:
    ' https://social.msdn.microsoft.com/Forums/vstudio/en-US/
    '   a9ef25ef-fada-4cbd-a341-f9eb22fb2f48/
    '   how-to-save-a-rich-text-into-a-sql-server-database-in-a-wpf-application?forum=wpf
    Using stream As New MemoryStream
        Try
            range.Save(stream, formato, True)
            Dim buffer = Encoding.UTF8.GetString(stream.ToArray())
            Using sw As New StreamWriter(_fileName, False, Encoding.Default)
                sw.Write(buffer)
            End Using

            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
    End Using
    'Using fStream As New FileStream(_fileName, FileMode.Create)
    '    Try
    '        range.Save(fStream, formato, True)
    '        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

Al final de la función tienes (comentado) el código anterior.

/// <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);

    // Para guardar con el formato que queramos
    // Adaptado de:
    // https://social.msdn.microsoft.com/Forums/vstudio/en-US/
    // a9ef25ef-fada-4cbd-a341-f9eb22fb2f48/
    // how-to-save-a-rich-text-into-a-sql-server-database-in-a-wpf-application?forum=wpf
    using (MemoryStream stream = new MemoryStream())
    {
        try
        {
            range.Save(stream, formato, true);
            var buffer = Encoding.UTF8.GetString(stream.ToArray());
            using (StreamWriter sw = new StreamWriter(_fileName, 
                                                      false, 
                                                      Encoding.Default))
            {
                sw.Write(buffer);
            }

            guardado = true;
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error el formato no es válido\r\n" + 
                            ex.Message, 
                            $"Guardar {formato}", 
                            MessageBoxButton.OK, 
                            MessageBoxImage.Asterisk);
        }
    }

    //    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;
}

Lo curioso del caso es que si lo guardaba como Rtf (DataFormats.Rtf) las vocales acentuadas se guardaban bien… Pero de esta forma, todo se guarda bien, al menos los tres formatos que he probado: Rtf, Xaml y Text.

Y es que en las pruebas que estaba haciendo usaba el código de ejemplo de la utilidad Compilar y ejecutar y tengo en varios sitios escrita la palabra versión y al guardarlo y después volver a abrirlo con formato Xaml, se mostraba como en la figura 1.

Figura 1. Los caracteres raros de la o con tilde ó

Al principio ni me fijé, pero cuando la ristra esa de caracteres raros se hizo más larga, ya que si me fijé

¡Como para no darme cuenta!

Y ya está… ahora modificaré la entrada anterior o pondré una aclaración para que vengas aquí (para que se vea que el Guille también se equivoca jajaja)

Espero que te sea de utilidad. Esa es la idea.

Nos vemos.
Guillermo

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

Scroll sincronizado en varios TextBox en WPF

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 vertical
Figura 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>
Private Sub SvFilas_ScrollChanged(sender As Object,
                                  e As ScrollChangedEventArgs)
    If inicializando Then Return

    inicializando = True

    Dim sv = TryCast(sender, ScrollViewer)
    If sv Is svFilas Then
        svCodigo.ScrollToVerticalOffset(e.VerticalOffset)
    Else
        svFilas.ScrollToVerticalOffset(e.VerticalOffset)
    End If

    inicializando = False
End Sub
/// <summary>
/// Sincronizar el scroll vertical del código y las filas
/// </summary>
private void 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>
Private Sub TxtCodigo_TextChanged(sender As Object,
                                  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
End Sub
/// <summary>
/// Al cambiar el texto del código actualizar el número de líneas
/// </summary>
private void 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!

Nos vemos.
Guillermo