el Guille, la Web del Visual Basic, C#, .NET y más...
Ir al índice de Visual Studio 2008 y .NET Framework 3.5 Utilidades .NET Framework 3.5

Actualización de gsCopia (v1.0.3.0)

 
Publicado el 16/Dic/2007
Actualizado el 17/Dic/2007
Autor: Guillermo 'guille' Som

Actualización de la utilidad gsCopia (v1.0.3.0 del 16/Dic/07). En esta revisión se permite usar unidades de destino fijas (que no sean extraíbles) además de no permitir que sea la unidad de arranque.



 

Introducción:

En esta revisión de la utilidad gsCopia, lo que he hecho es permitir indicar unidades de disco fijo como destino, aunque si esa unidad es la que se ha usado como arranque no se permite que se indique.

Lo de no permitir que se indique como destino la unidad usada por el sistema, es porque en Windows Vista no se permite guardar cosas en esa unidad, salvo que lo indiques expresamente tras un aviso de seguridad, así que... para evitar problemas he excluido de la búsqueda de unidades la que contenga el directorio del sistema.

Más abajo te explico como se hacen esas averiguaciones y comprobaciones.

 

Aquí tienes el código con los cambios que he hecho en esta revisión 1.0.3.0 y más abajo tienes el resto de links de las revisiones anteriores, la explicación detallada y el link para instalarlo con ClickOnce.

 

Empecemos viendo una captura de la aplicación en ejecución en la que veremos un par de cosas nuevas:

Figura 1. La utilidad en ejecución (versión 1.0.3.0)
Figura 1. La utilidad en ejecución (versión 1.0.3.0)

Como podemos ver en la figura 1, en las opciones de configuración tenemos una nueva opción: Permitir usar destinos no extraíbles, esa opción inicialmente tendrá un valor falso con idea de compatibilizar con los valores de las versiones anteriores de la utilidad, pero si la seleccionamos nos permitirá indicar como destino una unidad que no sea extraíble.

Si la unidad de destino no es extraíble, se avisa en la barra de estado, pero con color verde, para que no se vea como un error, solo para que se sepa que no se está usando una unidad extraíble.

Como te comentaba antes, ahora no se permite usar el disco de arranque como destino, por tanto, se hacen las comprobaciones correspondientes tanto en el código de arrastrar y soltar como en el de seleccionar, pero solo si el destino de esa selección (o de los ficheros soltados) es el combo de la unidad de destino.

Incluso al arrastrar al destino, si se está arrastrando algo de la unidad de arranque, se mostrará el cursor de prohibido, tal como puedes ver en la figura 2:

Figura 2. No se permite soltar cosas de la unidad de arranque
Figura 2. No se permite soltar cosas de la unidad de arranque

Lo que si se permite es indicar la unidad de arranque en el origen. En el caso de la figura 2, mi disco de arranque es el disco C.

Tanto en la figura 1 o la 2 puedes ver que ahora se muestran también los parámetros de la utilidad de copia, al menos si el contenido de ese grupo está contraído.

Veamos ahora el código que ha cambiado con respecto a la revisión de ayer.

Recuerda que el código que te muestro es solo el nuevo, el completo lo tienes en el ZIP con el proyecto o bien si vas uniendo el que he ido mostrando en las revisiones anteriores.

El nuevo código de la versión 1.0.3.0

Empecemos viendo el código del módulo UtilDiscos:

Imports vb = Microsoft.VisualBasic

Lo primero es agregar un alias al espacio de nombres de Microsoft.VisualBasic, con idea de usar las funciones propias de VB por medio de ese "alias", (en realidad solo se usa la función Len).

''' <summary>
''' Si se debe incluir la unidad de arranque al llamar a UnidadesExtraibles
''' (por defecto NO se incluye)
''' </summary>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public IncluirUnidadBoot As Boolean = False

''' <summary>
''' Comprueba si el path indicado está en la unidad de arranque
''' </summary>
''' <param name="pathAComprobar">
''' La ruta que se comprobará si está en el disco de arranque
''' </param>
''' <returns>
''' Un valor verdadero o falso según sea 
''' la unidad del path indicado el disco de arranque
''' </returns>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public Function EsBoot(ByVal pathAComprobar As String) As Boolean
    ' Si se indica un path vacío, se comprueba el directorio actual
    If vb.Len(pathAComprobar) = 0 Then
        pathAComprobar = Environment.CurrentDirectory
    End If
    If String.Compare(pathAComprobar.ToLower().Substring(0, 1), _
                      UnidadBoot.ToLower().Substring(0, 1)) = 0 Then
        Return True
    End If
    Return False
End Function

''' <summary>
''' El nombre de la unidad de arranque
''' (se asigna en el constructor)
''' </summary>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public ReadOnly UnidadBoot As String = "C:\"

''' <summary>
''' Constructor para saber cuál es la unidad de arranque
''' </summary>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Sub New()
    ' Averiguar cuál es la unidad del sistema
    Try
        UnidadBoot = System.IO.Path.GetPathRoot(Environment.SystemDirectory)
        Dim di As New System.IO.DirectoryInfo(Environment.SystemDirectory)
        UnidadBoot = di.Root.FullName
    Catch ex As Exception
        ' Si da error, usar el root del directorio del sistema
        UnidadBoot = System.IO.Path.GetPathRoot(Environment.SystemDirectory)
    End Try
End Sub

''' <summary>
''' Comprobar si la unidad del path indicado es extraíble
''' </summary>
''' <param name="pathAComprobar">
''' La ruta en la que se comprobará si es una unidad extraíble
''' </param>
''' <returns></returns>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public Function EsExtraible(ByVal pathAComprobar As String) As Boolean
    ' Si se indica un path vacío, se usa el directorio actual
    If vb.Len(pathAComprobar) = 0 Then
        pathAComprobar = Environment.CurrentDirectory
    End If
    Dim retType As TipoUnidades

    Try
        retType = GetDriveType(pathAComprobar)
    Catch ex As Exception
        retType = TipoUnidades.Desconocido
    End Try

    Return (retType = TipoUnidades.Extraible)
End Function

''' <summary>
''' Devuelve un array con las unidades extraibles
''' (en teoría para las llaves USB)
''' El nombre de cada unidad incluye la barra final, 
''' por ejemplo F:\
''' </summary>
''' <returns>
''' Devuelve un array con las unidades extraíbles
''' </returns>
''' <remarks></remarks>
Public Function UnidadesExtraibles() As String()
    Return UnidadesExtraibles(False)
End Function

''' <summary>
''' Devuelve un array con las unidades extraíbles y opcionalmente los fijos
''' El nombre de cada unidad incluye la barra final, 
''' por ejemplo F:\
''' </summary>
''' <param name="incluirFijos">
''' Un valor verdadero para incluir las unidades fijas
''' </param>
''' <returns>
''' Devuelve un array con las letras de las unidades
''' que coinciden con lo indicado
''' </returns>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public Function UnidadesExtraibles(ByVal incluirFijos As Boolean) As String()
    Dim extraibles As New List(Of String)
    Dim drives() As String = Environment.GetLogicalDrives()
    ' Solo comprobar si no se debe incluir
    ' (es decir, IncluirUnidadBoot sea False)
    Dim comprobadoBoot As Boolean = IncluirUnidadBoot

    For Each s As String In drives
        ' Si no se debe incluir la unidad de arranque
        ' (y no se ha comprobado ya)
        If comprobadoBoot = False AndAlso IncluirUnidadBoot = False Then
            If String.Compare(s.ToLower().Substring(0, 1), _
                              unidadBoot.ToLower().Substring(0, 1)) = 0 Then
                comprobadoBoot = True
                Continue For
            End If
        End If
        Dim retType As TipoUnidades = GetDriveType(s)
        If retType = TipoUnidades.Extraible _
        OrElse (incluirFijos = True AndAlso retType = TipoUnidades.Fijo) Then
            extraibles.Add(s)
        End If
    Next
    Return extraibles.ToArray
End Function

En realidad, este es casi todo el código del módulo, ya que solo falta la definición de la estructura y la función del API para comprobar el tipo de unidad... en fin...

Te explico las cosas que he añadido.

He añadido una variable pública (un campo que lo llaman) para indicar si se debe incluir el disco de arranque en las unidades devueltas (IncluirUnidadBoot). Inicialmente tiene un valor False, con idea de que no se incluya.

La función EsBoot sirve para comprobar si la unidad del path pasado como argumento es la unidad de arranque.
En esa función se comprueba que no se indique una cadena vacía, (ahí es donde se usa la función Len, aunque también se podría usar el método IsNullOrEmpty de la clase String, pero...). La comprobación se hace comparando esa unidad con la que se ha comprobado en el constructor del módulo (la variable UnidadBoot).

Esa variable (UnidadBoot) la he definido como de solo lectura, y se asigna en el constructor; recuerda que las variables (o campos) de solo lectura solo se pueden asignar en el constructor o al definirlas.

En el constructor se asigna esa variable usando el valor devuelto por Environment.SystemDirectory que nos dará el directorio del sistema, por ejemplo: C:\Windows\System32).
En realidad he "complicado" un poco más de la cuenta el código del constructor, ya que usando solo el código que hay en el Catch (o el que hay al principio del Try), sería suficiente, ya que, cuando se usa la variable UnidadBoot solo se comprueba la primera letra, pero... nunca está de más hacer comprobaciones extras...

El método (función) EsExtraible sirve para comprobar si una unidad es extraíble o no. El que esté en un Try es por si se produce un error al intentar averiguar el tipo de unidad. Esta función se usa desde el código del formulario.

La función UnidadesExtraibles la he cambiado para que acepte una sobrecarga, en la que se le indica si se deben incluir las unidades fijas, con idea de que se pueda devolver un array en el que incluya tanto las unidades extraíbles como las fijas, ya que en el código del formulario ahora se puede hacer una llamada a este método para que tenga en cuenta esas unidades fijas.

En el bucle que recorre todas las unidades devueltas por el método GetLogicalDrives de la clase Environment, se comprueba si se debe tener en cuenta la unidad de arranque (boot) o no.

Nota:
Si te preguntas porqué hago todas esas comprobaciones de cuál es la unidad de arranque en lugar de usar el disco C, es porque no siempre el disco C es el disco de arranque. Aunque en Windows Vista "normalmente" siempre es el disco C, pero en otros sistemas operativos, como el XP o anteriores, es posible que la unidad en la que está el sistema sea otra diferente.

Sigamos viendo los cambios en el código, ahora le toca el turno al formulario principal.

Public Sub New()

    ' This call is required by the Windows Form Designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    Dim fic As String

    With My.Application.Info
        Dim appData As String = _
                My.Computer.FileSystem.SpecialDirectories.MyDocuments & "\" & .ProductName
        If My.Computer.FileSystem.DirectoryExists(appData) = False Then
            My.Computer.FileSystem.CreateDirectory(appData)
        End If
        fic = appData & "\" & .ProductName & ".cfg"
    End With

    ' Crear el objeto de configuración
    cfg = New ConfigXml(fic, False)

    ' Para que quede claro que no se incluirá                   (16/Dic/07)
    ' la unidad de arranque
    UtilDiscos.IncluirUnidadBoot = False

End Sub

Te muestro el código completo del constructor, pero en realidad lo único que hay nuevo es la última asignación, que no es necesaria, pero como digo en los comentarios, para que quede claro que no se permitirá usar la unidad de arranque como disco de destino.

' Nuevo valor de configuración                          (16/Dic/07)
.DestinoNoUSB = cfg.GetValue("General", "DestinoNoUSB", .DestinoNoUSB)

Esta línea la agregas en el método leerCfg, justo después de asignar el valor de UtilidadCol.

Por supuesto, debes agregar a la configuración (Settings) el valor DestinoNoUSB con un valor predeterminado False.

En ese mismo método, casi al final (antes del End With), añade este otro código:

' Nuevo valor de configuración                          (16/Dic/07)
chkDestinoNoUSB.Checked = .DestinoNoUSB

Lo que te muestro ahora es para que lo incluyas en el método guardarCfg... espero que sepas dónde ponerlo...
Vale... como pista, el primero antes del .Save y el segundo antes del cfg.Save:

' Nuevo valor de configuración                          (16/Dic/07)
.DestinoNoUSB = chkDestinoNoUSB.Checked

 

' Nuevo valor de configuración                          (16/Dic/07)
cfg.SetValue("General", "DestinoNoUSB", .DestinoNoUSB)

Como te puedes imaginar, todo esto es para tener en cuenta los valores guardados en el fichero de configuración y para asignar el valor al CheckBox chkDestinoNoUSB del grupo de las configuraciones.
Vamos que esto es algo que habrá que hacer con cada nueva opción que pongamos... ¿no? pues eso... ;-)))

 

Private Sub hayUnidadExtraible()
    Static yaEstoy As Boolean

    ' No permitir la reentrada
    If yaEstoy Then Exit Sub

    yaEstoy = True

    Dim hayExtraible As Boolean = False
    ' Para ver si la unidad extraíble coincide con el destino
    Dim coinciden As Boolean = False
    Dim comprobarLaUnidad As Boolean = False
    Dim elExtraible As String = Me.cboDestino.Text
    If String.IsNullOrEmpty(elExtraible) = False Then
        elExtraible = elExtraible.Substring(0, 3)
        comprobarLaUnidad = True
    End If
    Try
        Dim unidades() As String
        ' Se pueden indicar que se usen los discos fijos        (16/Dic/07)
        unidades = UtilDiscos.UnidadesExtraibles(chkDestinoNoUSB.Checked)

        For Each dir As String In unidades
            If String.IsNullOrEmpty(dir) = False Then
                hayExtraible = True
                If comprobarLaUnidad = False Then
                    coinciden = False
                    Exit For
                Else
                    If dir = elExtraible Then
                        coinciden = True
                    End If
                End If
            End If
        Next
    Catch ex As Exception
    End Try

    If hayExtraible = False Then
        ' si no hay, usar un intervalo de 1 segundo
        timerComprobarExtraibles.Interval = 1000
    Else
        ' si hay, usar un intervalo de 3 segundos
        timerComprobarExtraibles.Interval = 3000
    End If

    statusComprobando.BackColor = Color.Red
    statusComprobando.ForeColor = Color.White
    statusComprobando.Visible = False

    If hayExtraible Then
        ' Si la unidad de destino no existe
        If coinciden = False Then
            statusComprobando.Text = "La unidad de destino no existe"
            statusComprobando.Visible = True
        Else
            ' Si la unidad de destino no es extraíble
            ' indicarlo como aviso, no como error
            If UtilDiscos.EsExtraible(Me.cboDestino.Text) = False Then
                statusComprobando.BackColor = Color.FromKnownColor(KnownColor.Control)
                statusComprobando.ForeColor = Color.Green
                statusComprobando.Visible = True
                statusComprobando.Text = "El destino no es extraíble"
            End If
        End If
    Else
        ' Mostrar el mensaje de forma más adecuada              (16/Dic/07)
        If chkDestinoNoUSB.Checked = False Then
            statusComprobando.Text = "NO HAY UNIDAD EXTRAIBLE"
        Else
            statusComprobando.Text = "No hay unidades fijas o extraíbles"
        End If
        statusComprobando.Visible = True
    End If

    ' El botón de copiar habilitarlo
    ' solo si hay unidades válidas y coincide con el destino
    btnCopiar.Enabled = coinciden

    yaEstoy = False
End Sub

Este otro código lo tienes que usar tal como está, y reemplaza al que ya había. Ya que aquí, además de tener en cuenta si se permiten unidades fijas (no USB), pues se tienen en cuenta, además de que ahora se muestra un aviso si el destino es una unidad fija, con idea de que el usuario sepa que no se está copiando a una unidad extraíble.

No te lo explico paso a paso, que ya es tarde, además de que supongo que sabrás que cambios son los que he hecho (tienen la fecha del 16) y... bueno, no hay nada especial que resaltar... simplemente observa el código y así sabrás qué es lo que se hace en cada caso.

Nota:
Aunque no te muestro el código, debes saber que debido a que he quitado el control chkComprobarDiscos, tendrás que quitar el código que lo usa en los métodos correspondientes, (no te preocupes que el editor de Visual Basic te avisa dónde lo estás usando).

Private Sub asignarFic(ByVal cboDir As Control, ByVal e As System.Windows.Forms.DragEventArgs)
    ' Para asignar todos los directorios soltados
    Dim dirs() As String
    dirs = CType(e.Data.GetData("FileDrop", True), String())
    Dim sFic As String = ""

    ' Si es el combo de destino                                 (16/Dic/07)
    ' comprobar si se debe permitir la unidad de arranque
    '
    ' En teoría esto no debe ocurrir,
    ' porque ya se comprueba en el DragEnter, pero...
    '
    If cboDir Is cboDestino Then
        Dim destinos As New List(Of String)

        For Each s As String In dirs
            ' Si es la unidad de arranque, no usarla
            ' si el valor de UtilDiscos.IncluirUnidadBoot es False
            If UtilDiscos.IncluirUnidadBoot = False _
            AndAlso UtilDiscos.EsBoot(s) Then
                Continue For
            Else
                destinos.Add(s)
            End If
        Next
        ' En la unidad de destino solo permitir un directorio   (16/Dic/07)
        ' (si no hay nada, dejar lo que hubiera)
        If destinos.Count > 0 Then
            sFic = destinos(0)
            cboDir.Text = sFic
        End If
        Exit Sub
    End If

    ' Aquí llegará si el combo no es el de destino
    sFic = String.Join(";", dirs)
    If String.IsNullOrEmpty(cboDir.Text) = False Then
        cboDir.Text &= ";" & sFic
    Else
        cboDir.Text = sFic
    End If
End Sub

Este método es el que se usa cuando se sueltan ficheros en un combo, así que, hay que tener en cuenta si es el de la unidad de destino (cboDestino), en cuyo caso, se comprueba si se debe permitir la unidad de arranque, etc.

Como digo en el comentario, en realidad no hace falta hacer ninguna comprobación, y en teoría podrías dejar el código que había antes, ya que al arrastrar los ficheros se comprueba si se debe permitir o no una operación de Drag & Drop.

' Mostrar la utilidad y los parámetros                  (16/Dic/07)
expanderHeader.Text = expanderHeader.Tag.ToString & _
                      " (" & System.IO.Path.GetFileName(cboUtilidad.Text) & _
                      " " & cboParametros.Text & ")"

Este código lo pones en el método expanderPic_Click y sustituye a la asignación que había antes, ya que ahora se deben mostrar los parámetros usados con la utilidad externa (antes solo se mostraba el nombre de la utilidad).

''' <summary>
''' Comprobar si alguna unidad extraíble tiene el fichero gsCopia.txt
''' de ser así, se usará como destino
''' Si no se encuentra ninguna, se usará la primera hallada
''' </summary>
''' <remarks></remarks>
Private Sub btnComprobarDestino_Click() Handles btnComprobarDestino.Click
    Dim laUnidad As String = ""

    Try
        Me.Cursor = Cursors.WaitCursor
        cboDestino.Text = ""
        statusInfo.Text = "Comprobando unidades extraíbles..."
        Me.Refresh()

        Dim unidades() As String
        ' Si se deben incluir los discos fijos                  (16/Dic/07)
        ' (salvo el de arranque)
        unidades = UtilDiscos.UnidadesExtraibles(chkDestinoNoUSB.Checked)

        For Each dir As String In unidades
            If String.IsNullOrEmpty(dir) = False Then
                ' Asignar a laUnidad, la primera unidad extraíble
                ' esto servirá para los casos en que ninguna tenga el fichero gsCopia.txt
                ' en ese caso, se usará la primera unidad extraíble.
                If String.IsNullOrEmpty(laUnidad) Then
                    laUnidad = dir
                End If

                Dim fic As String = dir & If(dir.EndsWith("\"), "", "\") & "gsCopia.txt"
                ' si da error, ignorarlo y seguir comprobando
                Try
                    If File.Exists(fic) Then
                        laUnidad = dir
                        Exit For
                    End If
                Catch ex As Exception
                End Try
            End If
        Next
    Catch ex As Exception
        MessageBox.Show("Error: " & ex.Message, _
                        "Comprobar unidades", _
                        MessageBoxButtons.OK, _
                        MessageBoxIcon.Exclamation)
    Finally
        Me.Cursor = Cursors.Default
        cboDestino.Text = laUnidad
        statusInfo.Text = statusInfo.Tag.ToString

        ' Si no hay unidad extraíble,
        ' deshabilitar el botón de copia
        btnCopiar.Enabled = Not String.IsNullOrEmpty(laUnidad)
        If btnCopiar.Enabled = False Then
            statusInfo.Text = "¡Atención, no hay unidades extraíbles!"
        End If
    End Try
End Sub

En el código anterior, todo esto estaba en otro método independiente, pero ahora lo he incluido dentro del método de evento, ya que ese otro método no se usaba en más sitios.

En realidad, lo único que ha cambiado es la forma de llamar al método UnidadesExtraibles de la clase UtilDiscos, y es que ahora se le pasa si se deben tener en cuenta o no las unidades fijas (además de las extraíbles).

Private Sub btnSeleccionarDes_Click() Handles btnSeleccionarDes.Click
    Dim oFD As New System.Windows.Forms.FolderBrowserDialog
    With oFD
        .Description = "Seleccionar el directorio de destino"
        .RootFolder = Environment.SpecialFolder.MyComputer
        If .ShowDialog = System.Windows.Forms.DialogResult.OK Then
            ' No permitir usar el disco de arranque             (16/Dic/07)
            ' (si así está en UtilDiscos)
            If UtilDiscos.IncluirUnidadBoot = False AndAlso UtilDiscos.EsBoot(.SelectedPath) Then
                MessageBox.Show("No se puede seleccionar la unidad de arranque como destino", _
                                "Copiar", _
                                MessageBoxButtons.OK, _
                                MessageBoxIcon.Exclamation)
                cboDestino.Focus()
            Else
                Me.cboDestino.Text = .SelectedPath
            End If
        End If
    End With
End Sub

Este es el nuevo código del método de evento para seleccionar el destino, que como puedes observar, se comprueba si se debe tener en cuenta la unidad de arranque. Aquí es uno de los sitios en el que se usa el método EsBoot de la clase UtilDiscos, con idea de saber si esa unidad es la de arranque o no.

' Quitado cobDestino.DragEnter,                                 (16/Dic/07)
' ya que se comprueba por separado
Private Sub Form1_DragEnter(ByVal sender As Object, _
                            ByVal e As DragEventArgs) _
                            Handles Me.DragEnter, _
                                    cboOrigen.DragEnter, cboUtilidad.DragEnter
    ' Drag & Drop, comprobar con DataFormats
    If e.Data.GetDataPresent(DataFormats.FileDrop) Then
        e.Effect = DragDropEffects.Copy
    End If
End Sub

' Comprobar por separado en el combo de destino                 (16/Dic/07)
Private Sub cboDestino_DragEnter(ByVal sender As Object, _
                                 ByVal e As DragEventArgs) _
                                 Handles cboDestino.DragEnter
    If e.Data.GetDataPresent(DataFormats.FileDrop) Then
        ' Para asignar todos los directorios soltados
        Dim dirs() As String
        dirs = CType(e.Data.GetData("FileDrop", True), String())

        ' Comprobar si se indica el disco de arranque 
        ' y no se permite usarlo
        If UtilDiscos.IncluirUnidadBoot = False Then
            ' Por si se arrastran varias y alguna no es el boot
            ' (no se cómo lo harán, pero... nunca se sabe, je, je)
            Dim todasBoot As Boolean = True

            For Each s As String In dirs
                If UtilDiscos.EsBoot(s) = False Then
                    todasBoot = False
                    Exit For
                End If
            Next
            If todasBoot Then
                e.Effect = DragDropEffects.None
                Exit Sub
            End If
        End If

        e.Effect = DragDropEffects.Copy
    End If
End Sub

Aunque el primer método de estos dos que te muestro (sí, los de arriba, ¿es que no te habías enterado que normalmente estoy explicando las cosas después de mostrar el código? si es que... en fin... menos mal que no soy el único despistado, je, je). Como te decía, el primer método sigue igual, solo que ahora no se usa para detectar el evento DragEnter del control cboDestino, ya que ese evento se comprueba en el segundo método, con idea de hacer la comprobación (si es necesaria) de que no se pueda arrastrar nada de la unidad de arranque, de esa forma, se podrá mostrar el icono de prohibido si se intenta hacer eso... (ver la figura 2).

 

Y estos son todos los cambios... de todas formas, recuerda que en el ZIP con el proyecto completo, está todo esto ya hecho, pero bueno, por si te interesa ir viendo "en vivo" las cosas que he cambiado...

Nota:
Aunque estos cambios los publico en realidad el 17 de Diciembre (a la una y pico de la madrugada), debido a que empecé a escribir todo esto a las 19 horas del día 16, pues... en todos los links aparece el día 16, que es en realidad cuando terminé de modificar el código de la utilidad, así que... para no ponerme a cambiarlo todo, lo dejo como 16.

 

Y esto es todo... por ahora...

 

Nos vemos.
Guillermo


Código de ejemplo (comprimido):

Pulsa estos links para ver todo lo relacionado con esta utilidad:

Nota:
Tanto los ZIPs como la instalación con ClickOnce incluyen la última versión.


 


La fecha/hora en el servidor es: 03/11/2024 16:33:25

La fecha actual GMT (UTC) es: 

©Guillermo 'guille' Som, 1996-2024