Etiqueta: WPF

  • 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 Sarcastic smile
    El que avisa… Nerd smile

    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.
    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# Winking smile

    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 Winking smile

    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) In love

    // ----------------------------------------------------------------------------
    // 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 Be right back

    '------------------------------------------------------------------------------
    ' 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 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.
    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! Smile

    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 Smile with tongue out

    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 Winking smile

    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 Smile

    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

  • Recomendaciones para adaptar los controles (y la fuente) en aplicaciones WPF (2ª parte el código para C#)

    Pues eso… lo prometido es deuda… aquí tienes el código para C# de las Recomendaciones para adaptar los controles (y la fuente) en aplicaciones WPF.
    También te enseñaré el código XAML modificado o rectificado desde la última vez, es decir, desde que ayer publiqué el original Winking smile

    Esta es la ventana de la aplicación en modo diseño (figura 1).

    Figura 1. La ventana principal en modo diseño
    Figura 1. La ventana principal en modo diseño

    Los cambios en el código XAML que he puesto son menores, para que los botones de seleccionar de los paneles superiores no se peguen demasiado a las esquinas de la derecha, para ello he cambiado el valor de Padding del control GroupBox y he reducido el margen (Margin) de la parte izquierda de los botones. También he puesto el ancho de la primera columna del ListView a 220, ya que hay espacio suficiente (la ventana principal tiene asignado un valor mínimo de 830 de ancho.

    Por si te sirve de ayuda, sobre todo si has usado padding y margin en los estilos CSS.

    Comentarte que los valores de Padding y Margin en Xaml se refieren a las posiciones:
    left, top, right, bottom,
    a diferencia de las usadas en los estilos CSS, que hacen referencia a:
    top right bottom left.
    De nada Be right back

    Aquí tienes el código XAML (del diseñador de WPF) del panel superior izquierdo, el de la derecha es prácticamente igual.

    <GroupBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="1" 
              Header="Unidad 1" Margin="0" Padding="0,0,4,0">
        <Grid DockPanel.Dock="Top" Margin="0" >
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="30"/>
                <RowDefinition Height="4"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Label Content="Directorio:" Grid.ColumnSpan="1" Margin="0,4,0,0" />
            <TextBox Name="txtDir1" Grid.Row="0" Grid.Column="1" 
                     Grid.ColumnSpan="3" Grid.RowSpan="1"
                     Margin="0,6,0,0" KeyDown="txtDir1_KeyDown"
                     TextWrapping="NoWrap" Text="S:\iPhone"/>
            <Button Name="btnSel1" Content=" Seleccionar... " 
                    Grid.Column="4" Grid.ColumnSpan="1"
                    Margin="4,6,0,0" Click="BtnSel_Click" />
            <ListView Name="lvFics1" Grid.Row="2"
                      Grid.ColumnSpan="5" Grid.RowSpan="1" 
                      Margin="0,8,0,0">
                <ListView.View>
                    <GridView >
                        <GridViewColumn Header="Archivo" Width="220"
                                        DisplayMemberBinding="{Binding Nombre}"/>
                        <GridViewColumn Header="Fecha" Width="Auto"
                                        DisplayMemberBinding="{Binding Fecha}"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </Grid>
    </GroupBox>
    

    El ListView (concretamente cada GridViewColumn) definen un enlace (Binding) a Nombre y Fecha, de esa forma podremos agregar a cada elemento del ListView un objeto definido por nosotros (ItemFic) con esas dos propiedades y se asignarán correctamente a cada una de las columnas del ListView.

    Esta es la definición de las clases ItemFic y ItemDir, esta última la usaré para mostrar el nombre del directorio del ListView definido en el panel inferior.

    El método llenar es el que se encarga de asignar los elementos a los ListView.

    class ItemDir
    {
        public string Nombre { get; set; }
    }
    
    class ItemFic : ItemDir
    {
        public DateTime Fecha { get; set; }
    }
    
    /// <summary>
    /// Llenar un listview con los ficheros del directorio
    /// </summary>
    private void llenar(ListView lvFiles, string dir)
    {
        lvFiles.Items.Clear();
        var dirI = new sio.DirectoryInfo(dir);
        if (dirI.Exists == false)
            return;
    
        foreach (var fi in dirI.GetFiles())
        {
            var lvi = new ItemFic() { Nombre = fi.Name, Fecha = fi.LastWriteTime };
            lvFiles.Items.Add(lvi);
        }
    }
    

    Nota:
    sio es un alias al espacio de nombres System.IO ya que Path también está definido en System.Windows.Shapes y así no tenemos conflictos de nombres, al menos si usas C#, ya que al crear un proyecto WPF se añade esa importación de espacios de nombres.

    Cambiar el tamaño de la ventana principal y las letras

    Para gestionar el cambio de tamaño de la ventana y las letras de cada control he añadido un par de menús a la aplicación.

    Recuerda que para que los tamaños de las letras (FontSize) se hagan medio-automáticamente, tal como lo hago en esta aplicación, deben tener el valor predeterminado de las fuentes, es decir, NO añadas ningún valor a las fuentes de los controles o de los contenedores, si no, no funcionará.

    Este es el código XAML de la definición de los menús.

    <Menu x:Name="menu" Grid.Row="0" Grid.Column="0" 
          Grid.ColumnSpan="2" 
          Height="24" Margin="0,0,0,4">
        <MenuItem x:Name="mnuCambiarFuente" 
                  Header="_Tamaño y Fuente">
            <MenuItem Header="Tamaño _Ventana">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Label Content="Cambiar tamaño" 
                           ToolTip="Puedes usar un valor negativo para reducir" />
                    <TextBox x:Name="txtTamañoVentana" Text="0,2" 
                             Grid.Column="1" Width="40"
                             Margin="8,6,0,0"/>
                    <Button x:Name="btnAplicar" Content=" Aplicar " 
                            Click="BtnAplicarVentana_Click"
                            Grid.Column="2" Margin="8,6,0,0" />
                </Grid>
            </MenuItem>
            <MenuItem Header="Tamaño _Fuente">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Label Content="Cambiar tamaño" 
                           ToolTip="Puedes usar un valor negativo para reducir" />
                    <TextBox x:Name="txtTamañoFuente" Text="1,0" 
                             Grid.Column="1" Width="40"
                             Margin="8,6,0,0"/>
                    <Button x:Name="btnAplicarFuente" Content=" Aplicar " 
                            Click="BtnAplicarFuente_Click"
                            Grid.Column="2" Margin="8,6,0,0" />
                </Grid>
            </MenuItem>
        </MenuItem>
    
     </Menu>
    

    Aquí tienes el código de C# para cambiar el tamaño de las fuentes, se suma o sustrae el valor que indiquemos (si este último es negativo), y en el tamaño de la ventana principal añadimos (o restamos) el porcentaje indicado.

    private double meHeight;
    private double meWidth;
    private double meFontSize;
    
    private void BtnAplicarVentana_Click(object sender, RoutedEventArgs e)
    {
        // Para cambiar el tamaño a partir del valor inicial
        if (meHeight == 0)
            meHeight = this.Height;
        if (meWidth == 0)
            meWidth = this.Width;
    
        double d = 0;
        double.TryParse(txtTamañoVentana.Text, out d);
        this.Height = nuevoTamaño(meHeight, d);
        this.Width = nuevoTamaño(meWidth, d);
    }
    
    private void BtnAplicarFuente_Click(object sender, RoutedEventArgs e)
    {
        if (meFontSize == 0)
            meFontSize = this.FontSize;
        double d = 0;
        double.TryParse(txtTamañoFuente.Text, out d);
        this.FontSize = meFontSize + d;
    }
    
    
    private double nuevoTamaño(double actual, double incremento)
    {
        return actual * incremento + actual;
    }
    

    Quitar el botón maximizar de la ventana usando API

    Para terminar, te mostraré el código para quitar el botón de maximizar la ventana, ya que WPF no tiene opciones para ocultar ese botón, salvo que cambiemos el tipo de ventana, pero en este caso yo la tengo definida asignando a la propiedad ResizeMode el valor CanResizeWithGrip. Por tanto, puedo maximizar, minimizar, cambiar el tamaño, etc.

    Pero siempre queda bien eso de que el usuario no pueda maximizar de golpe, si quiere la ventana más grande, que le cambie el tamaño poco a poco Winking smile

    // --------------------------------------------------------------------------
    // Código para quitar el botón de maximizar
    // Adaptado de la versión de C# publicada en:
    // https://stackoverrun.com/es/q/5101958
    // 
    [DllImport("user32.dll")]
    private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
    
    [DllImport("user32.dll")]
    private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
    
    private const int GWL_STYLE = -16;
    private const int WS_MAXIMIZEBOX = 0x10000;
    
    private void Window_SourceInitialized(object sender, EventArgs e)
    {
        var hwnd = new WindowInteropHelper((Window)sender).Handle;
        var value = GetWindowLong(hwnd, GWL_STYLE);
        SetWindowLong(hwnd, GWL_STYLE, (int)(value & ~WS_MAXIMIZEBOX));
    }
    

    Los eventos, SourceInitialized y otros, están definidos en el código Xaml de la aplicación.

    Height="500" Width="850"
    MinHeight="460" MinWidth="830"
    WindowStartupLocation="CenterScreen"
    ResizeMode="CanResizeWithGrip"
    
    SourceInitialized="Window_SourceInitialized"
    
    Loaded="MainWindow_Loaded">
    

    Y esto es todo lo que quería contarte usando los puntos y comas In love

    Te recomiendo que te leas lo escrito en la primera parte, ya que aunque el código mostrado es para Visual Basic, las explicaciones, particularmente para el código XAML, te van a servir, seguro Smile

    Y para que vayas a la otra página, allí está el enlace para descargar el zip con el código completo, tanto para Visual Basic como para C#.

    Nos vemos.
    Guillermo