Archivo de la etiqueta: WinForms

Compilar código en .NET Core (.NET 5.0) para consola y Windows Forms

Pues eso… usando la aplicación que te mostré el otro día (Compilar y ejecutar versión para .NET Core (.NET 5.0)) y después de hacer varias pruebas con otra DLL que compila el código y lo ejecuta sin usar Process.Start, al menos si es una aplicación de consola, (ya que para ejecutar las de Windows Forms, al menos hasta lo que yo he probado, solo puedo lanzarla usando Process.Start); al final me he quedado con la llamada a dotnet para crear el proyecto, compilarlo y ejecutar el código compilado.

Nota:
Abajo te dejo un ZIP con el código fuente del proyecto (Compilar y ejecutar) y la utilidad para colorear (gsColorearCore).
En la carpeta pruebas del proyecto está el código fuente (de VB y C#) de la aplicación de ejemplo.

Compilar con dotnet (desde la línea de comandos)

En las primeras pruebas (las del código que te puse en el post arriba mencionado, usaba aplicaciones de consola, más que nada porque así se lo decía al usar el comando:

dotnet new console -o "AppDir" -lang c#|vb

Ese código lo que hace es crear una aplicación de consola en el directorio indicado por AppDir (si ya existe, podemos añadir –force al final para que genere el contenido aunque sobreesciba lo que ya hubiese.).

Para crear/compilar una aplicación para Windows Forms tendremos que usar una línea de comandos parecida a la anterior, pero indicando winforms en vez de console.
En el siguiente código creamos una aplicación de .NET 5.0 Core usando Visual Basic en el directorio E:\Guille\source\repos\MiAppWinF.

Nota:
Si no tienes instalado el .NET 5.0 y tienes al menos el .NET Core 3.0 o 3.1 simplemente usa el lenguaje C# en lugar de Visual Basic, ya que en esas versiones anteriores al 5.0 solo se permiten aplicaciones de Windows Forms o WPF para C#.

dotnet new winforms -o "E:\Guille\source\repos\MiAppWinF" -lang vb

Nota:
En realidad poner el directorio entre comillas dobles no es necesario, salvo que se utilice el comando dotnet build.

Una vez hecho esto, solo tendremos que modificar el código generado y si queremos que se ejecute el contenido de ese directorio podemos usa la siguiente línea de comandos:

dotnet run -p E:\Guille\source\repos\MiAppWinF

El código usado en la aplicación Compilar y ejecutar NETCore

En el código de la aplicación compilar y ejecutar para .NET Core 5.0 este último paso me lo salto y lo que hago es crear la aplicación (con –force) sustituyo el código. ya que –force o simplemente al crear con new se generan los ficheros en blanco (sin el código que ya tuviera).
En su lugar utilizo build para compilar el código.
Es decir, creo la aplicación con new (uso –force por si ya existiera), guardo el código fuente del editor del programa, lo compilo con build y finalmente lo ejecuto usando Process.Start.

El comando build usado es el siguiente:

dotnet build "E:\Guille\source\repos\MiAppWinF"

Las comillas dobles solo son necesarias si el path o el nombre del proyecto contiene espacios.

Y ese comando habrá creado un ejecutable (aparte de la DLL que siempre genera .NET Core) en el directorio: E:\Guille\source\repos\MiAppWinF\bin\Debug\net5.0-windows cuyo nombre será MiAppWinF.exe
Fíjate que al ser una aplicación para Windows (las aplicaciones de Windows Forms y las de WPF solo se pueden usar en plataformas Windows, es decir, no se pueden usar ni en Linux ni en MacOS) se crea en un directorio aparte de las aplicaciones de consola, que sería en: bin\Debug\net5.0\

Nota:
El directorio de salida puede ser diferente si antes de build (y después de new) cambias la configuración del proyecto.

Un poco de código fuente por favor

El código del método compilar al que se le pasa el código a compilar/ejecutar.

''' <summary>
''' Compilar el código indicado en el parámetro.
''' 
''' Usando dotnet (.NET Core) se hará lo siguiente:
''' Definir un directorio para VB o C#: MiApp_VB o MiApp_CS
''' Usar el comando:
''' dotnet new console -o dir -lang C#|VB --force
''' Copiar en ese directorio el fichero como Program.vb o .cs
''' Usar el comando (y redirigir la salida):
''' dotnet run -p dir
''' </summary>
Private Sub compilar(texto As String, mostrarSoloSalida As Boolean)
    ' Compilar usando la línea de comandos
    Dim ext = If(optCS.IsChecked, "cs", "vb")

    Dim appDir = System.IO.Path.Combine(System.Environment.CurrentDirectory,
                                        $"MiApp_{ext}")

    ' crear un fichero temporal para compilar
    Dim ficProgram = Path.Combine(appDir, $"Program.{ext}")

    Dim dotnet = "dotnet"
    Dim args = "--version"
    txtSalida.Text = $"{dotnet} {args} = {ejecutar(dotnet, args, waitSecs:=5000)}"

    ' Considero que es aplicación de Windows Forms              (07/Sep/20)
    ' si contiene InitializeComponent
    Dim esWF = texto.IndexOf("InitializeComponent()") > -1
    If esWF Then
        args = $"new winforms -o ""{appDir}"" -lang {If(optCS.IsChecked, "c#", "vb")} --force"
    Else
        args = $"new console -o ""{appDir}"" -lang {If(optCS.IsChecked, "c#", "vb")} --force"
    End If

    If mostrarSoloSalida Then
        ejecutar(dotnet, args, waitSecs:=10000)
    Else
        txtSalida.Text &= $"{vbCrLf}{vbCrLf}{ejecutar(dotnet, args, waitSecs:=10000)}"
    End If

    ' Eliminar todos los ficheros del lenguaje
    ' (solo quedará el que se guarde)
    Dim files = Directory.GetFiles(appDir, $"*.{ext}")
    For i = 0 To files.Length - 1
        File.Delete(files(i))
    Next

    ' Guardar el código a compilar
    Using sw As New System.IO.StreamWriter(ficProgram,
                                           False,
                                           System.Text.Encoding.UTF8)
        sw.Write(texto)
    End Using

    args = $"build ""{appDir}"""

    If mostrarSoloSalida Then
        Dim res = ejecutar(dotnet, args, waitSecs:=10000)
        Dim hayErrorComp = False
        If optVB.IsChecked Then
            If res.Contains("error BC") Then
                hayErrorComp = True
            End If
        Else
            If res.Contains("error CS") Then
                hayErrorComp = True
            End If
        End If
        If hayErrorComp Then
            txtSalida.Text &= vbCrLf & res
            txtSalida.Text &= $"{vbCrLf}--- FIN DE LA COMPILACIÓN CON ERRORES ---"
            Return
        End If
        'txtSalida.Text &= $"{vbCrLf}{vbCrLf}{res}"
    Else
        txtSalida.Text &= $"{vbCrLf}{vbCrLf}{ejecutar(dotnet, args, waitSecs:=10000)}"
    End If

    Dim exe = System.IO.Path.Combine(appDir, $"bin\Debug\net5.0\MiApp_{ext}.exe")
    ' Si es aplicación de Windows Forms, usa otro directorio    (07/Sep/20)
    If esWF Then
        'net5.0-windows
        exe = System.IO.Path.Combine(appDir, $"bin\Debug\net5.0-windows\MiApp_{ext}.exe")
        txtSalida.Text &= $"{vbCrLf}{vbCrLf}Aplicación de Windows"
    End If
    txtSalida.Text &= $"{vbCrLf}{vbCrLf}{ejecutar(exe, esWinF:=esWF)}"

    txtSalida.Text &= $"{vbCrLf}--- FIN DE LA COMPILACIÓN ---"

    txtSalida.SelectionStart = txtSalida.Text.Length
    txtSalida.SelectionLength = 0
End Sub

Del método ejecutar tengo dos versiones, la primera es la que uso para llamar al dotnet y en el que redirijo la salida de la consola para capturarla y mostrarla en la caja de textos txtSalida.
Al primero le paso el nombre de la aplicación a ejecutar (dotnet) y los argumentos.
Al segundo le paso el nombre del ejecutable y si es o no aplicación de Windows, en cuyo caso se usa ShellExecute y el estilo de la ventana es normal, aparte de no usar Kill para que no se cierre la aplicación al poco de haberse abierto 🙂

Private Function ejecutar(exe As String, arg As String,
                          Optional conKill As Boolean = False,
                          Optional waitSecs As Integer = 10000) As String
    Dim res = ""
    Using 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 waitSecs segundos para que le de tiempo a ejecutarse
            ' como mínimo esperar 2000 (o 5000 si se usa dotnet)
            If waitSecs < 2000 Then waitSecs = 2000
            p.WaitForExit(waitSecs)

            If conKill Then
                p.Kill()
            End If

            res = p.StandardOutput.ReadToEnd()
        Catch ex As Exception

            res = ex.Message
        End Try
    End Using

    Return res
End Function

''' <summary>
''' Ejecutar el código y mostrarlo en la ventana de salida.
''' </summary>
Private Function ejecutar(exe As String,
                          Optional waitSecs As Integer = 2000,
                          Optional esWinF As Boolean = False) As String
    Dim p As New Process

    p.StartInfo.FileName = exe

    If esWinF Then

        With p.StartInfo
            .UseShellExecute = True
            .WindowStyle = ProcessWindowStyle.Normal
        End With

        ' Iniciamos el proceso
        p.Start()

        ' Esperar a que el proceso finalice
        '
        ' Esperamos 2 segundos para que le de tiempo a ejecutarse
        ' Como mínimo 2 segundos
        If waitSecs < 2000 Then waitSecs = 2000
        p.WaitForExit(waitSecs)

        Return ""
    End If

    ' Indicamos que queremos redirigir la salida
    p.StartInfo.RedirectStandardOutput = True
    ' Para redirigir la salida, UseShellExecute debe ser falso
    p.StartInfo.UseShellExecute = False

    ' No usar esto
    ' salvo que se use p.Kill()
    p.StartInfo.CreateNoWindow = True

    ' Iniciamos el proceso
    p.Start()

    ' Esperar a que el proceso finalice
    '
    ' Esperamos 2 segundos para que le de tiempo a ejecutarse
    ' Como mínimo 2 segundos
    If waitSecs < 2000 Then waitSecs = 2000
    p.WaitForExit(waitSecs)
    Try
        p.Kill()
    Catch ex As Exception
        Debug.WriteLine(ex.Message)
    End Try

    ' Convertir la salida usando el código de página 437
    ' que es la usada en MS-DOS (línea de comandos)
    Dim res = p.StandardOutput.ReadToEnd()

    Return res
End Function

Recomendación para usar código de Windows Forms en la utilidad de Compilar y ejecutar

Por último, comentarte que si quieres compilar una app para Windows Forms desde la utilidad de compilar y ejecutar, el código de esa aplicación debe estar en un solo fichero, en el que te recomiendo que incluyas el método Main, la definición del diseñador de Windows Forms y el código que realmente quieres usar.
Esto es así porque el programa/utilidad no está preparado para usar múltiples ficheros.

Te muestro a continuación cómo sería ese código para Visual Basic y para C# y una captura de la salida realizada al compilar desde la utilidad.

Este es el código de Visual Basic del formulario de pruebas.

Imports System
Imports System.Windows.Forms

'
' El punto de entrada del programa
'

Friend Module Program

    <STAThread()>
    Friend Sub Main(args As String())
        Application.SetHighDpiMode(HighDpiMode.SystemAware)
        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.Run(New Form1)
    End Sub

End Module

'
' El diseñador del formulario
'

<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class Form1
    Inherits System.Windows.Forms.Form

    'Form overrides dispose to clean up the component list.
    <System.Diagnostics.DebuggerNonUserCode()> _
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    'Required by the Windows Form Designer
    Private components As System.ComponentModel.IContainer

    'NOTE: The following procedure is required by the Windows Form Designer
    'It can be modified using the Windows Form Designer.  
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()> _
    Private Sub InitializeComponent()
        Me.TextBox1 = New System.Windows.Forms.TextBox()
        Me.Button1 = New System.Windows.Forms.Button()
        Me.Label1 = New System.Windows.Forms.Label()
        Me.Button2 = New System.Windows.Forms.Button()
        Me.SuspendLayout()
        '
        'TextBox1
        '
        Me.TextBox1.Location = New System.Drawing.Point(12, 12)
        Me.TextBox1.Name = "TextBox1"
        Me.TextBox1.Size = New System.Drawing.Size(182, 23)
        Me.TextBox1.TabIndex = 0
        '
        'Button1
        '
        Me.Button1.Location = New System.Drawing.Point(200, 11)
        Me.Button1.Name = "Button1"
        Me.Button1.Size = New System.Drawing.Size(75, 23)
        Me.Button1.TabIndex = 1
        Me.Button1.Text = "Button1"
        Me.Button1.UseVisualStyleBackColor = True
        '
        'Label1
        '
        Me.Label1.Location = New System.Drawing.Point(12, 38)
        Me.Label1.Name = "Label1"
        Me.Label1.Size = New System.Drawing.Size(263, 23)
        Me.Label1.TabIndex = 2
        Me.Label1.Text = "Label1"
        '
        'Button2
        '
        Me.Button2.Location = New System.Drawing.Point(200, 90)
        Me.Button2.Name = "Button2"
        Me.Button2.Size = New System.Drawing.Size(75, 23)
        Me.Button2.TabIndex = 3
        Me.Button2.Text = "Cerrar"
        Me.Button2.UseVisualStyleBackColor = True
        '
        'Form1
        '
        Me.AcceptButton = Me.Button1
        Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
        Me.CancelButton = Me.Button2
        Me.ClientSize = New System.Drawing.Size(287, 125)
        Me.Controls.Add(Me.Button2)
        Me.Controls.Add(Me.Label1)
        Me.Controls.Add(Me.Button1)
        Me.Controls.Add(Me.TextBox1)
        Me.Name = "Form1"
        Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
        Me.Text = "Form1"
        Me.ResumeLayout(False)
        Me.PerformLayout()

    End Sub

    Friend WithEvents TextBox1 As TextBox
    Friend WithEvents Button1 As Button
    Friend Label1 As Label
    Friend Button2 As Button
End Class

'
' El código del formulario
'

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        TextBox1.Text = "<tu nombre>"

        'AddHandler Button2.Click, AddressOf Button2_Click
        AddHandler Button2.Click, Sub() Me.Close()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim str = TextBox1.Text
        If str = "<tu nombre>" Then
            str = " amigo"
        End If
        Label1.Text = $"¡Hola {ToUpperFirst(str)}!"
    End Sub

    ''' <summary>
    ''' Convierte en mayúsculas el primer carácter de la cadena indicada.
    ''' </summary>
    Private Function ToUpperFirst(str As String) As String
        'Return str(0).ToString().ToUpper() & str.Substring(1)
        if str="" then str=" amigo"
        Return str(0).ToString.ToUpper & str.Substring(1)
    End Function

    'Private Sub Button2_Click(sender As Object, e As EventArgs)
    '    Me.Close()
    'End Sub
End Class

Este es el código de C# del formulario de pruebas.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;

//
// El punto de entrada del programa
//

static class Program
{
    /// <summary>
    ///  The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.SetHighDpiMode(HighDpiMode.SystemAware);
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}


//
// El diseñador del formulario
//

partial class Form1
{
    /// <summary>
    ///  Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    ///  Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    ///  Required method for Designer support - do not modify
    ///  the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.textBox1 = new System.Windows.Forms.TextBox();
        this.button1 = new System.Windows.Forms.Button();
        this.label1 = new System.Windows.Forms.Label();
        this.button2 = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // textBox1
        // 
        this.textBox1.Location = new System.Drawing.Point(12, 12);
        this.textBox1.Name = "textBox1";
        this.textBox1.Size = new System.Drawing.Size(182, 23);
        this.textBox1.TabIndex = 0;
        // 
        // button1
        // 
        this.button1.Location = new System.Drawing.Point(200, 12);
        this.button1.Name = "button1";
        this.button1.Size = new System.Drawing.Size(75, 23);
        this.button1.TabIndex = 1;
        this.button1.Text = "button1";
        this.button1.UseVisualStyleBackColor = true;
        this.button1.Click += new System.EventHandler(this.button1_Click);
        // 
        // label1
        // 
        this.label1.Location = new System.Drawing.Point(12, 38);
        this.label1.Name = "label1";
        this.label1.Size = new System.Drawing.Size(263, 23);
        this.label1.TabIndex = 2;
        this.label1.Text = "label1";
        // 
        // button2
        // 
        this.button2.Location = new System.Drawing.Point(200, 90);
        this.button2.Name = "button2";
        this.button2.Size = new System.Drawing.Size(75, 23);
        this.button2.TabIndex = 3;
        this.button2.Text = "Cerrar";
        this.button2.UseVisualStyleBackColor = true;
        //this.button2.Click += new System.EventHandler(this.button1_Click);
        // 
        // Form1
        // 
        this.AcceptButton = this.button1;
        this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.CancelButton = this.button2;
        this.ClientSize = new System.Drawing.Size(287, 125);
        this.Controls.Add(this.label1);
        this.Controls.Add(this.button1);
        this.Controls.Add(this.textBox1);
        this.Controls.Add(this.button2);
        this.Name = "Form1";
        this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
        this.Text = "Form1";
        this.Load += new System.EventHandler(this.Form1_Load);
        this.ResumeLayout(false);
        this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.TextBox textBox1;
    private System.Windows.Forms.Button button1;
    private System.Windows.Forms.Label label1;
    private System.Windows.Forms.Button button2;
}

//
// El código del formulario
//

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        textBox1.Text = "<tu nombre>";
        
        //button2.Click += => (object o, EventArgs e) this.Close();
        //button2.Click += delegate (object sender, EventArgs e) { this.Close(); };
        button2.Click += (sender, e) => { this.Close(); };
    }

    private void button1_Click(object sender, EventArgs e)
    {
        //label1.Text = $"Hola {ToUpperFirst(textBox1.Text)}!";
        var str = textBox1.Text;
        if( str == "<tu nombre>" ) str = " amigo";
        label1.Text = $"Hola {ToUpperFirst(str)}!";
    }

    /// <summary>
    /// Convierte en mayúsculas el primer carácter de la cadena indicada.
    /// </summary>
    private string ToUpperFirst(string str)
    {
        if(str=="") str = " amigo";
        return str[0].ToString().ToUpper() + str.Substring(1);
    }
}

Esta es la salida del código ejecutándose con la utilidad de compilar y ejecutar para NETCore.

Figura 1. El resultado de compilar y ejecutar una aplicación de Windows Forms para .NET Core 5.0.

Y esto es todo… tengo algo más por ahí, para seguir con la compilación y ejecución del código para .NET Core, pero eso será para otra ocasión 🙂

 

Espero que te haya sido de utilidad… Esa es la idea y ¡eso espero! 🙂

 

Nos vemos.
Guillermo

El ZIP con el código fuente:

ZIP: Compilar_ejecutar_NetCore_20200908_1551.zip (71.8 KB)
MD5 checksum: 52A9AD9A1F1605547D1C8451CADD4CF0

Si quieres que tu aplicación se muestre en el monitor externo dile que se centre en la pantalla (CenterScreen)

Pues eso… que estuve un tiempo buscando soluciones para que se mostrasen mis aplicaciones en el monitor secundario (que es el que uso como principal) y resulta que la solución es más simple que todo eso… sí, solo con indicarle que se centre en la pantalla (CenterScreen) es suficiente Winking smile

Y esto vale tanto para aplicaciones de Windows Forms como para las de WPF (Windows Presentation Foundation).

En WPF lo haces con este código en el diseñador de la ventana (Window):

WindowStartupLocation = "CenterScreen"

En WinForms asigna a la propiedad StartPosition del formulario de inicio el valor CenterScreen.

Y ya está… ya no tengo más que contarte Winking smile

Nos vemos.
Guillermo

Recomendaciones para adaptar los controles (y la fuente) en aplicaciones WPF

Nota: (02/Ene/2019)
Ya puedes ver la segunda parte de este artículo, principalmente con el código para C# y un par de cambios en el código XAML, así como un consejo sobre la diferencia en los valores de Padding y Margin entre el código XAML y el de las hojas de estilo CSS.
He puesto un ZIP (al final de esta página) con el código completo para Visual Basic y C#.

Pues eso… lo primero, lo primero: ¡FELIZ AÑO NUEVO! 🙂
Aunque esto lo estoy escribiendo el 31 de diciembre de 2018 (17:21), pero lo pondré para que se publique el 1 de enero de 2019… eso si es que me da tiempo a escribirlo entero hoy… guiño

Pero te digo lo que contendrá esto que acabas de empezar a leer: Unas indicaciones sobre cómo añadir los controles para que al cambiar el tamaño del formulario o ventana, éstos se adapten al nuevo tamaño, y de paso, veremos cómo cambiar también el tamaño de las fuentes y comprobaremos que como ocurría en las aplicaciones de Windows Forms (o casi) los controles también se adaptan al nuevo tamaño.

Lo más importante es: NO ASIGNAR FUENTES PERSONALIZADAS a los controles (al menos en el tipo de fuente), no, no te estoy gritando, es que quiero que quede claro, ya que esa ha sido una de mis pesadillas… y después resulta que todo es más fácil si no usaba mis propias fuentes… guiño

La batallita del Guille o los antecedentes

Para que no te lleves a engaños, te voy a explicar cómo yo lo estoy haciendo ahora y sobre todo pensando que siempre (o casi) he programado con los formularios usando WinForm de Visual Studio y con Visual Basic en un 99,99% de las aplicaciones que he empezado a hacer, después las convierto a C#, pero las empiezo con Visual Basic.

Cuando diseño el formulario, suelo poner los controles en sitios específicos, si viene al caso suelo usar controles TabControl y GroupBox y escasamente paneles y casi nunca TableLayoutPanel, que ya me he dado cuenta de que no existe el equivalente en WPF y por tanto, las veces que lo he usado para facilitar el paso de WinForms a WPF, pues… no me ha servido de mucho 🙂

Nota:
El TableLayoutPanel tiene como equivalencia el Grid de los controles de WPF.

Para facilitar la transición de WinForms a WPF/XAML he probado de todo, lo más cercano a lo que quería era usar Canvas y posicionar directamente los controles donde yo los quería, pero esto no vale… ya que lo que siempre he ido buscando es poder adaptar los controles al cambiar el tamaño del formulario.

Lo mejor que he encontrado es el control Grid, que si se tiene cuidado al añadir los controles dentro y se configuran (más o menos) bien las definiciones de filas y columnas (y el nombre de la fuente o lo que es lo mismo la propiedad FontFamily) se pueden conseguir cosas decentemente aceptables. Y eso es lo que te voy a explicar aquí… 🙂

Como verás, te estoy poniendo enlaces a las clases que menciono en la documentación de Microsoft y con referencia a .NET 4.7.2 (espero que tarden en romper los enlaces 🙂 )

Una aplicación WPF de ejemplo (y de paso útil)

El otro día pensé hacer una aplicación para copiar el contenido (las fotos) de los dos móviles que tengo y pensé en complicarme la vida haciendo la aplicación con WPF guiño

No voy a entrar en detalles, pero quise acceder al contenido de los móviles, pero no hubo forma, y eso que busqué cómo hacerlo, pero… no di con la tecla, así que… se me ocurrió crear dos carpetas en un disco externo de 5 TB que tengo y desde ahí copiarlas al portátil. El hacer este paso intermedio es porque yo guardo las fotos en una carpeta del año y dentro de esa carpeta las fotos las guardo según el día y el móvil usado. Por ejemplo, para las fotos hechas hoy día 31 de diciembre crearía una carpeta llamada 12 31 1 iP7 para el iPhone y otra llamada 12 31 2 P2XL para el Pixel 2. El 1 y el 2 es porque cuando voy creando carpetas del mismo día las voy enumerando, ya que cuando tenía la GoPro (R.I.P.) también las añadía a una carpeta. Ahora al hacer la aplicación que automatiza la creación de las carpetas he dejado como fijo el 1 para el iPhone de Apple y el 2 para el Pixel de Google.

El diseño empecé haciéndolo con Grid y creando 2 filas y dos columnas, con idea de en la parte superior izquierda poner un grupo de controles para el iPhone con el directorio donde están las fotos, un botón de seleccionar y un ListView mostrando las fotos a copiar; y en el de la derecha los mismos controles para el Pixel 2. Y en la parte inferior la selección de la fecha desde la que quiero empezar a copiar y el directorio donde almacenarlas, aparte de un ListView mostrando las carpetas creadas y algunas cosillas más, que te mostraré ahora cuando veas el formulario en tiempo de diseño.

El formulario en tiempo de diseño

Figura 1. La ventana en tiempo de diseño con WPF.
Figura 1. La ventana en tiempo de diseño con WPF.

Como puedes ver en la figura 1, están los controles que te he comentado, aparte el botón de Iniciar Copia, y las cajas de texto para indicar el formato de los nombres de los directorios según sean para el iPhone o para el Pixel 2. Además del habitual botón para Salir o cerrar la aplicación, cosas de la costumbre, ya que en realidad no es necesario, pero bueno…

Fíjate también que he añadido un menú llamado Tamaño y Fuente, que nos servirá para hacer las pruebas de cambiar el tamaño de la fuente y del formulario o ventana (me acostumbraré a decir ventana, ya que en WPF es una clase Window y no un Form como en WinForms).

En los tres paneles que hay en esa ventana he usado un control del tipo GroupBox. Cada uno de esos contenedores definen también un Grid. Y los controles están distribuidos en las columnas y filas de cada una de esas rejillas.

Uno de los problemas que me encontré fue que en WPF no existe el equivalente a FolderBrowserDialog de System.Windows.Forms. y se ve que no hay equivalencia, lo más que he encontrado es un control de usuario que hace las veces (o eso dice la página donde lo encontré, pero no he llegado a probarlo). Buscando más por internet, he leído que en el espacio de nombres Microsoft.Win32 existen clases para acceder a los diálogos comunes, al menos al de selección de ficheros, pero tampoco lo he comprobado (acabo de mirarlo y no, no hay para seleccionar directorios), aunque me lo he apuntado para ver cómo funciona y qué es lo que hay, pero eso será tema de otro artículo.

Para poder usar el FolderBrowserDialog debes añadir una referencia a System.Windows.Forms y si es necesario añadir una importación del espacio de nombres, yo he optado por añadir esa importación llamándola frm de esa forma estaré seguro que las clases y demás cosas que define no se mezclan con las clases que se recomiendan usar con las aplicaciones de WPF.

Esta sería la declaración de ese espacio de nombres (por ahora solo en VB ya que aún no tengo convertida la aplicación en C#, pero lo haré guiño)

' Para FolderBrowserDialog y OpenFileDialog
Imports frm = System.Windows.Forms

Y ya que estoy con esto, me vas a permitir que antes de ver todo el tema del diseño te muestre el código para seleccionar el directorio, ya que es interesante, porque como los tres botones Seleccionar hacen lo mismo, solo que interactúan con distintas cajas de texto, pues… ahí tienes un truco, que lo mismo ya lo conoces, pero… guiño

Private Sub BtnSel_Click(sender As Object, e As RoutedEventArgs) Handles _
                                    btnSel1.Click, btnSel2.Click, btnSelDest.Click
    Dim dir = ""
    If sender Is btnSel1 Then
        dir = txtDir1.Text
    ElseIf sender Is btnSel2 Then
        dir = txtDir2.Text
    Else
        dir = txtDirDest.Text
    End If

    Dim desc = If(sender Is btnSel1, " para la unidad 1",
                    If(sender Is btnSel2, " para la unidad 2", "de destino"))

    Dim fb As New frm.FolderBrowserDialog
    With fb
        .Description = "Selecciona el directorio" & desc
        .RootFolder = Environment.SpecialFolder.MyComputer
        .SelectedPath = dir
        .ShowNewFolderButton = False
        If .ShowDialog() = frm.DialogResult.OK Then
            dir = .SelectedPath
            If sender Is btnSel1 Then
                txtDir1.Text = dir
                My.Settings.dir1 = dir
                llenar(lvFics1, dir)
            ElseIf sender Is btnSel2 Then
                txtDir2.Text = dir
                My.Settings.dir2 = dir
                llenar(lvFics2, dir)
            Else
                My.Settings.dir3 = dir
                txtDirDest.Text = dir
            End If
        End If
    End With
End Sub

No te voy a explicar nada más, espero que sea fácil de comprender viendo el código.

Pasemos al tema del diseño.

Las cosas a tener en cuenta al diseñar usando el control Grid

Lo importante es no asignar estas cosas a los controles que pongamos dentro del Grid: FontName, Width ni Height. Dejar el valor predeterminado para HorizontalAlignment y VerticalAlignment que es Stretch. Ni al Grid tampoco.

Con este sencillo consejo conseguirás todo lo que deseas conseguir, ¡seguro! guiño

Seguramente te preguntarás: ¿Cómo posicionar los controles donde queramos?
Pues… usando Grid.Column y Grid.Row para indicar en que cuadrícula estará cada control y si viene al caso (por ejemplo para que la caja de textos o el ListView) ocupen más espacio usaremos Grid.ColumnSpan y Grid.RowSpan.

Además de la propiedad Margin, para que deje un poco de espacio donde lo necesitemos.

Veamos cómo está definido el Grid del panel superior izquierdo.

Este es el código XAML del diseñador de la ventana.

<GroupBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="1" 
          Header="Unidad 1" Margin="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" />
        <TextBox Name="txtDir1" Grid.Row="0" Grid.Column="1" 
                 Grid.ColumnSpan="3" Grid.RowSpan="1"
                 Margin="0,6,0,0" TextWrapping="NoWrap" Text="S:\iPhone"/>
        <Button Name="btnSel1" Content=" Seleccionar... " 
                Grid.Column="4" Grid.ColumnSpan="1"
                Margin="8,6,0,0" />
        <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 GruopBox está contenido en el Grid principal, el valor de Grid.Row = «1» es porque en la fila cero está el menú. Aquí le he puesto Grid.Column = «0» pero realmente no es necesario, ya que si no se indica qué fila o columna ocupa, será siempre cero. Lo mismo ocurre con Grid.ColumnSpan = «1», como mínimo espanea (¿existe ese palabro?) una columna o una fila.

Los controles de este GroupBox están contenidos en un Grid. Yo he definido 5 columnas y 3 filas. El poner 5 columnas, principalmente es para que el TextBox ocupe 3 y el Label de la izquierda y el Button de la derecha ocupen una columna cada uno. El ListView ocupa una fila completa (antes tenía más filas, pero no es necesario). La fila pequeña, con 4 de alto es para dejar un espacio entre los controles de arriba con la lista, pero esto también se podría haber solucionado con los márgenes, pero… bueno… ahí está 🙂

Con los Margin lo que logro es que tanto el botón como el TextBox no se hagan muy altos y se alineen mejor con la etiqueta.

Como puedes comprobar, el único Width (ancho) específico que he puesto es el de la primera columna del ListView. Pero más que nada para que al verlo en tiempo de diseño o al iniciar la aplicación y estando vacío se vea más espacioso, ya que al llenarse, se ajustará al texto que contenga.

En las figuras 2 y 3 tienes la diferencia de asignar Auto al tamaño de la columna (figura 2) y ponerlo como te muestre en este código con 220 de ancho (figura 3). En el panel derecho está en Auto y se ajusta al nombre de la imagen.

Figura 2. El ancho de las columnas del ListView se ajustan automáticamente al contenido.
Figura 2. El ancho de las columnas del ListView se ajustan automáticamente al contenido.
Figura 3. El ancho de la columna izquierda está puesto a 220 y el de la derecha en Auto.
Figura 3. El ancho de la columna izquierda está puesto a 220 y el de la derecha en Auto.

La cuestión es que prefiero darle como mínimo unos 200 o 220 pixeles de ancho guiño

Otro punto a resaltar en ese código es cómo se maneja el contenido del ListView, no voy a entrar en mucho detalle porque ya lo expliqué en mi sitio hace 6 años y 34 días (el 27 de diciembre de 2012) en este artículo: Ejemplo de ListView y equivalencia a subitems, solo decirte que he creado dos clases (dentro de la clase principal) para manejar dicho contenido.

Este es el código de las dos clases (por ahora en VB):

Class ItemDir
    Public Property Nombre As String
End Class

Class ItemFic
    Inherits ItemDir
    Public Property Fecha As Date
End Class

Y estas clases las uso de esta forma a la hora de asignar el contenido de los ListView ligados con los directorios de mas fotos y con el de las carpetas creadas, por eso hay dos clases, una para los directorios y la segunda para los ficheros. El código para mostrar las fotos está en el método llenar.

''' <summary>
''' Llenar un listview con los ficheros del directorio
''' </summary>
Private Sub llenar(lvFiles As ListView, dir As String)
    lvFiles.Items.Clear()
    Dim dirI = New DirectoryInfo(dir)
    If dirI.Exists = False Then Exit Sub

    For Each fi In dirI.GetFiles()
        Dim lvi = New ItemFic With {.Nombre = fi.Name, .Fecha = fi.LastWriteTime}
        lvFiles.Items.Add(lvi)
    Next
End Sub

Como ves, solo se añade el objeto creado a cada elemento del ListView y el Binding, concretamente el DisplayMemberBinding, se encarga del resto.

La parte inferior de la ventana es más de lo mismo. No te la voy a explicar al completo porque ya sería muy pesado, podrás ver el código completo cuando lo publique, aunque no sé si esperaré a que esté el de C# o publicaré primero el de Visual Basic y después el de C#.

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

En el menú he puesto dos opciones (ahora te enseño el código XAML) una para poder cambiar el tamaño de la ventana principal y el otro para cambiar el tamaño de las letras.

A dicho menú le he añadido una etiqueta, una caja de textos y un botón (esto también se puede hacer en los menús de WinForms).

Este es el código XAML de la definición del menú principal y los dos submenú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>

Como ves, hay Grid hasta en la sopa 🙂

El guión bajo en el Header de los menús es para indicar que se resalte esa letra (la que le sigue) al pulsar la tecla Alt, es lo mismo que el ampersand (&) en las aplicaciones de WinForms.

Veamos el código para Visual Basic para cada uno de los botones:

Private meHeight As Double
Private meWidth As Double
Private meFontSize As Double

Private Sub BtnAplicarVentana_Click(sender As Object, e As RoutedEventArgs)
    ' Para cambiar el tamaño a partir del valor inicial
    If meHeight = 0 Then
        meHeight = Me.Height
    End If
    If meWidth = 0 Then
        meWidth = Me.Width
    End If

    Dim d As Double = 0
    Double.TryParse(txtTamañoVentana.Text, d)
    Me.Height = nuevoTamaño(meHeight, d)
    Me.Width = nuevoTamaño(meWidth, d)
End Sub

Private Sub BtnAplicarFuente_Click(sender As Object, e As RoutedEventArgs)
    If meFontSize = 0 Then
        meFontSize = Me.FontSize
    End If
    Dim d As Double = 0
    Double.TryParse(txtTamañoFuente.Text, d)
    Me.FontSize = meFontSize + d
End Sub

Como ves ninguno de los dos métodos de evento tienen la cláusula Handles, esto es porque en el código XAML ya hemos indicado qué método se usará para cada uno de los botones.

En el tamaño de la fuente simplemente añadimos el valor indicado al tamaño que ya tuviera, mientras que el alto y ancho de la ventana usamos un incremento porcentual (o casi).

Este es el código del método nuevoTamaño:

Private Function nuevoTamaño(actual As Double, incremento As Double) As Double
    Return actual * incremento + actual
End Function

En el código hay también unas llamadas al API de Windows para ocultar el botón Maximizar de la ventana, ya que de forma nativa WPF no tiene forma de ocultar ese botón, al menos si la ventana se puede cambiar de tamaño, que es el caso de la que usa este programa, que tiene el valor CanResizeWithGrip asignado a la propiedad ResizeMode.
Aunque como leí en uno de los foros que estuve consultando: ¿Para qué quieres quitar el botón maximizar si tu formulario puede cambiar de tamaño? Y tiene razón, pero… en fin… costumbres heredadas de los formulari0s de Windows Forms guiño

Este es el código para Visual Basic de esas API

'--------------------------------------------------------------------------
' 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 Shared Function GetWindowLong(ByVal hWnd As IntPtr,
                                      ByVal nIndex As Integer) As Integer
End Function

<DllImport("user32.dll")>
Private Shared Function SetWindowLong(ByVal hWnd As IntPtr, ByVal nIndex As Integer,
                                      ByVal dwNewLong As Integer) As Integer
End Function

Private Const GWL_STYLE As Integer = -16
Private Const WS_MAXIMIZEBOX As Integer = &H10000

Private Sub Window_SourceInitialized(ByVal sender As Object,
                                     ByVal e As EventArgs) Handles Me.SourceInitialized
    Dim hwnd = New WindowInteropHelper(CType(sender, Window)).Handle
    Dim value = GetWindowLong(hwnd, GWL_STYLE)
    SetWindowLong(hwnd, GWL_STYLE, CInt((value And Not WS_MAXIMIZEBOX)))
End Sub
'
'--------------------------------------------------------------------------

Y esto es todo.

Son las 22:55 del lunes 31 de Diciembre de 2018, pero… ya sabes lo pondré en automático para que se publique maña uno de enero de 2019 guiño

Y mañana o pasado pondré el enlace al código completo para Visual Basic y espero que también el de C#.

¡Feliz Año Nuevo!

Nos vemos.
Guillermo


El código completo del ejemplo
(solución para Visual Studio usando .NET 4.7.2)

El ZIP con el código completo (una solución de Visual Studio 2017 con los proyectos de Visual Basic y C#

El zip: Copiar_archivos_de_2_unidades.zip (214 KB)

MD5 Checksum: 2E3B3F34410F37D051DC5AED8FBBCE1A