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

El código fuente de la utilidad gsCopia para Visual Basic 2008

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

El código fuente de la utilidad gsCopia para Visual Basic 2008 (este código es del de la versión 1.0.1.0, para ver los cambios realizados, mira en las páginas de las distintas actualizaciones).



 

Introducción:

En esta página te muestro el código fuente para Visual Basic 2008 de la aplicación gsCopia.

Si quieres saber más de esta aplicación y bajarte tanto el ejecutable (para .NET Framework 2.0) como el proyecto para Visual Basic 2008, puedes hacerlo desde estos enlaces (links):

Nota:
El código aquí mostrado es el de la versión 1.0.1.0.
Los cambios realizados en las revisiones posteriores está en las páginas de esas revisiones.
Los ZIPs con el ejecutable y el código, así como la instalación ClickOnce siempre contienen la última versión.

 

Aquí tienes los links a las distintas partes del código

 

La clase para saber las unidades extraíbles que hay en el equipo

Esta clase está basada en la que publiqué hace unas semanas: Funciones del API: GetDriveType y GetLogicalDrives.

Pero la he simplificado para que use el método GetLogicalDrives de la clase Environment para saber las unidades lógicas que hay instaladas en el equipo. Aunque para saber si la unidad es extraíble, utilizo la función del API de Windows GetdriveType, ya que en .NET Framework 2.0 (al menos que yo sepa) no hay una función equivalente.

Esta clase en realidad es un módulo llamado UtilDiscos que solo tiene un método público: UnidadesExtraibles, que devuelve un array de tipo String con los nombres de las unidades extraíbles que hay en el equipo. El nombre de cada unidad incluye la barra de directorio, es decir, está en el formato F:\.

'------------------------------------------------------------------------------
' UtilDiscos                                                        (11/Dic/07)
' Comprobar las unidades extraíbles instaladas en el equipo
'
' Usando API para el tipo de las unidades
' Usando la clase Environment para las unidades instaladas
'
' Basado en el código publicado en mi sitio:
' http://www.elguille.info/NET/dotnet/GetDriveType_GetLogicalDrives_API.aspx
'
' Mostrar las unidades libres y ocupadas                            (03/Nov/07)
' y el tipo de unidades que son
'
' Nota:
' En Windows Vista debe ejecutarse como administrador, incluso en el IDE,
' si no, dará un error de que "requiere elevación" (de privilegios).
' Con Visual Studio 2008 no da ningún error en Windows Vista
'
' ©Guillermo 'guille' Som, 2007
'------------------------------------------------------------------------------
Option Strict On

Imports Microsoft.VisualBasic
Imports System
Imports System.Collections.Generic

Imports System.Runtime.InteropServices

Module UtilDiscos

    ''' <summary>
    ''' Enumeración para los tipos de unidades devueltos por la función GetDriveType
    ''' </summary>
    ''' <remarks></remarks>
    Private Enum TipoUnidades As Integer
        ' Indico también los valores del API (empiezan por cero y van de uno en uno)
        Desconocido     ' 0 DRIVE_UNKNOWN       The drive type cannot be determined.
        No_montado      ' 1 DRIVE_NO_ROOT_DIR   The root path is invalid;
        '                                       for example, there is no volume mounted at the path.
        Extraible       ' 2 DRIVE_REMOVABLE     The drive has removable media;
        '                                       for example, a floppy drive or flash card reader.
        '                                       Las llaves USB los da como extraibles.
        Fijo            ' 3 DRIVE_FIXED         The drive has fixed media;
        '                                       for example, a hard drive, flash drive, or thumb drive.
        '                                       Los discos duros normales enchufados por USB son fijos.
        Remoto          ' 4 DRIVE_REMOTE        The drive is a remote (network) drive. 
        CDROM           ' 5 DRIVE_CDROM         The drive is a CD-ROM drive.
        RAMDISK         ' 6 DRIVE_RAMDISK       The drive is a RAM disk.
    End Enum

    ''' <summary>
    ''' Para saber el tipo de unidad
    ''' </summary>
    ''' <param name="nDrive">
    ''' El nombre de la unidad a comprobar
    ''' </param>
    ''' <returns>
    ''' Un valor de la enumeración TipoUnidades con el tipo de la unidad
    ''' </returns>
    ''' <remarks>
    ''' El uso de esta función requiere privilegios de administrador
    ''' </remarks>
    <DllImport("kernel32.dll")> _
    Private Function GetDriveType(ByVal nDrive As String) As TipoUnidades
    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></returns>
    ''' <remarks></remarks>
    Public Function UnidadesExtraibles() As String()
        Dim extraibles As New List(Of String)
        Dim drives() As String = Environment.GetLogicalDrives()

        For Each s As String In drives
            Dim retType As TipoUnidades = GetDriveType(s)
            If retType = TipoUnidades.Extraible Then
                extraibles.Add(s)
            End If
        Next
        Return extraibles.ToArray
    End Function
End Module

 

El formulario principal

En este código del formulario principal (al igual que en el de configuración), utilizo una nueva característica que tiene Visual Basic 2008 para los casos en que no se usen los parámetros de los métodos de evento, y es: ¡no indicarlos!

También uso la "función" If, que es el equivalente a la función IIF de VB, aunque la función If, devuelve un tipo adecuado a los parámetros indicados, mientras que IIF siempre devuelve Object, por tanto, (si eres de los míos y tienes siempre Option Strict On) tendrías que hacer una conversión al tipo de destino.

Nota:
Ninguna de estas dos características está soportada en Visual Basic 2005, por tanto, si copias y pegas este código en VB2005, seguramente te dará algún que otro error, pero que no son complicados de solucionarlos (al menos a mi me parece que no son difíciles, pero a ti no lo sé, je, je).

El código te lo voy a ir mostrando poco a poco, con idea de ir explicando las cosas que hace ese código (aunque he intentado poner comentarios para que resulte más fácil de comprenderlo).

Nota:
Si vas copiando cada parte del código que te voy mostrando, las puedes ir pegando en ese mismo orden, es decir, muestro TODO el código, pero con explicaciones de cada parte, con idea de que te resulte más claro de entender... esto no es algo a lo que te debes acostumbrarte ya que no siempre lo haré así... que esto es más trabajoso que dejarte todo el código... pero hoy me ha dado por ahí... je, je.

Empecemos por el principio.
Aquí tienes el código con los comentarios generales de la utilidad, el copyright y las importaciones de los espacios de nombres usados en este formulario.
También tienes explicación de algunos de los parámetros usados con las dos utilidades "predeterminadas" de copia: robocopy y xcopy.

Por cierto, xcopy se incluye con todos los sistemas operativos, pero robocopy no.
En la página principal de esta utilidad tienes un link para bajarte la versión de robocopy.exe para Windows XP y Windows 2003 Server.
En Windows Vista (al menos en la versión Ultimate) se incluye "de fábrica".
En caso de que copies la utilidad robocopy.exe en un Windows que no la trae, debes darte cuenta de ponerla en un directorio incluido en el PATH con idea de que no tengas que indicar el nombre completo para poder usarla.

 

'------------------------------------------------------------------------------
' gsCopia                                                           (10/Dic/07)
' Utilidad para hacer copias de directorios en unidades extraíbles
'
' Esta utilidad usa utilidades como xcopy y robocopy que copian directorios completos.
' Mis parámetros preferidos para esas utilidades son:
' xcopy.exe origen destino /D /S /C /I /H /R /K /Y /Q
'   /D Copia solo los modificados recientemente
'   /S incluye subdirectorios
'   /C continua si hay errores
'   /I crea la estructura de directorios
'   /H copia los ocultos y los del sistema
'   /R sobrescribe los de solo lectura
'   /K copia los atributos (si no, se quitan los de solo lectura)
'   /Y no pide confirmación para sobrescribir
'   /Q no muestra lo que se está copiando
'   /G permite la copia de ficheros encriptados a sistemas que no lo permiten
'   /O copia la información del propietario y el ACL
'
' robocopy.exe origen destino /MIR /R:10 /W:4 /COPY:DAT (o /COPYALL) /LOG+:fic.log
'   /MIR hace una copia espejo del directorio (borra lo que no esté en origen)
'   /R:10 hace 10 intentos si da error al copiar
'   /W:4 si hay error, espera 4 segundos entre cada intento
'   /COPYALL copia todo... como /COPY:DATSOU
'   /COPY:DAT
'   /LOG+:fic.log añade la salida del programa al fichero log indicado
'               para sobrescribir el log, usar /LOG:fic.log
'   Los valores de /COPY son:
'       D=Data, A=Attributes, T=Timestamps, S=Security=NTFS ACLs, O=Owner info, U=aUditing info
'
' ©Guillermo 'guille' Som, 2007
'------------------------------------------------------------------------------
Option Strict On
'Option Infer Off

Imports Microsoft.VisualBasic
Imports vb = Microsoft.VisualBasic
Imports System
Imports System.Windows.Forms
Imports System.Drawing

Imports System.IO
Imports System.Collections.Generic
Imports System.Text
Imports System.Diagnostics

Public Class Form1
    Private cancelar As Boolean = False

    Private expanderExpanded As Boolean = True
    Private expanderSize As Size
    Private expander2Expanded As Boolean = True
    Private expander2Size As Size

Las variables declaradas se usan en varios métodos y ya irás viendo para que se usan.

Private Sub leerCfg()
    With My.Settings
        If .Location.X > -1 Then
            Me.Location = .Location
            Me.Size = .Size
        End If
        cboDestino.Text = .Destino
        string2Cbo(.DestinoCol, cboDestino)
        cboOrigen.Text = .Origen
        string2Cbo(.OrigenCol, cboOrigen)
        cboUtilidad.Text = .Utilidad
        string2Cbo(.UtilidadCol, cboUtilidad)
        cboParametros.Text = .Parametros
        string2Cbo(.ParametrosCol, cboParametros)

        chkOcultarVentana.Checked = .OcultarVentana

        ' No cambiar este valor (siempre debe ser False)
        .ComprobarDiscos = False
        chkComprobarDiscos.Checked = .ComprobarDiscos
        chkComprobarDiscos.Enabled = False

        expanderExpanded = .Expandido
        ' Las nuevas opciones de la versión 1.0.1.0 (12/Dic/07)
        expander2Expanded = .Expandido2

        chkMostrarDestino.Checked = .MostrarDestino
        chkNoSeleccionar.Checked = .NoSeleccionar
        chkNoModificar.Checked = .NoModificar
    End With
End Sub

Private Sub guardarCfg()
    With My.Settings
        If Me.WindowState = FormWindowState.Normal Then
            .Location = Me.Location
            .Size = Me.Size
        Else
            .Location = Me.RestoreBounds.Location
            .Size = Me.RestoreBounds.Size
        End If

        ' Actualizar el contenido de los combos
        ' para añadir el texto si no está en la lista
        actualizarCombos()

        .Destino = cboDestino.Text
        .DestinoCol = combo2String(cboDestino)
        .Origen = cboOrigen.Text
        .OrigenCol = combo2String(cboOrigen)
        .Utilidad = cboUtilidad.Text
        .UtilidadCol = combo2String(cboUtilidad)
        .Parametros = cboParametros.Text
        .ParametrosCol = combo2String(cboParametros)

        .OcultarVentana = chkOcultarVentana.Checked

        ' No cambiar este valor
        .ComprobarDiscos = False
        '.ComprobarDiscos = chkComprobarDiscos.Checked

        .Expandido = expanderExpanded

        ' Las nuevas opciones de la versión 1.0.1.0 (12/Dic/07)
        .Expandido2 = expander2Expanded

        .MostrarDestino = chkMostrarDestino.Checked
        .NoSeleccionar = chkNoSeleccionar.Checked
        .NoModificar = chkNoModificar.Checked

        .Save()
    End With

End Sub

Estos dos métodos los uso para leer los valores de la configuración y asignar los valores correspondientes a cada control (leerCfg) y para guardar los valores según los que tengan esos controles (guardarCfg).

Lo que quiero destacar de este código es lo siguiente:

Los elementos de las listas de cada combo los guardo en la configuración como una cadena "normal" (podría haber usado una colección, pero...), y para separar cada elemento lo hago con una barra vertical (|), por tanto, he creado dos funciones (ahora te las muestro). Una de esas funciones (string2Cbo) asigna a los combos el contenido de una cadena en la que cada elemento a añadir está separado por la barra vertical y otro (combo2String) que hace lo contrario, es decir, convierte los elementos de un combo en una cadena separada por la barra esa vertical.

Por lo demás, lo único que se hace es asignar adecuadamente los valores de configuración a los controles y viceversa.

Aquí tienes esas dos funciones en las que se usan String.Join para unir el contenido de un array en una cadena con el separador de la barra vertical y el método Split de una cadena para hacer lo contrario:

''' <summary>
''' Devuelve el contenido del combo indicado como una cadena (string)
''' con cada elemento del combo separado por la barra vertical
''' </summary>
''' <returns></returns>
''' <remarks>
''' Esta función se usa internamente en este control
''' para devolver los contenidos de los diferentes controles
''' </remarks>
Private Function combo2String(ByVal cbo As ComboBox) As String
    Dim lista As New List(Of String)
    For Each s As String In cbo.Items
        lista.Add(s)
    Next
    Return String.Join("|", lista.ToArray())
End Function

''' <summary>
''' Asigna la cadena con los elementos a asignar al combo indicado.
''' Cada elemento estará separado por una barra vertical
''' </summary>
''' <param name="datos">
''' La cadena con los elementos sepados por una barra vertical
''' </param>
''' <param name="cbo">
''' El comboBox al que se asignarán esos elementos
''' </param>
''' <remarks>
''' Este método se usa internamente en este control
''' para asignar una cadena a cada uno de los combos usados
''' </remarks>
Private Sub string2Cbo(ByVal datos As String, ByVal cbo As ComboBox)
    Dim ar() As String = datos.Split("|".ToCharArray, _
                                     StringSplitOptions.RemoveEmptyEntries)
    cbo.Items.Clear()
    For Each s As String In ar
        cbo.Items.Add(s)
    Next
End Sub

 

''' <summary>
''' Actualizar el contenido de los combos
''' de forma que si el texto no está en los elementos (Items)
''' se añada a la lista
''' </summary>
''' <remarks></remarks>
Private Sub actualizarCombos()
    With cboDestino
        If .Items.Contains(.Text) = False Then
            .Items.Add(.Text)
        End If
    End With

    With cboOrigen
        If .Items.Contains(.Text) = False Then
            .Items.Add(.Text)
        End If
    End With

    With cboParametros
        If .Items.Contains(.Text) = False Then
            .Items.Add(.Text)
        End If
    End With

    With cboUtilidad
        If .Items.Contains(.Text) = False Then
            .Items.Add(.Text)
        End If
    End With
End Sub

El método actualizarCombos lo llamo antes de guardar los valores, con idea de que si había algo en la propiedad Text del combo y no está en la lista de elementos, pues... que los añada.

''' <summary>
''' Comprobar las unidades extraíbles para ver cual tiene
''' el fichero gsCopiar.txt en el raíz
''' (si hay más de una, se usa la primera)
''' </summary>
''' <remarks></remarks>
Private Sub comprobarDestino()
    Dim laUnidad As String = ""

    Try
        Dim unidades() As String
        unidades = UtilDiscos.UnidadesExtraibles()


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

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

Este método comprueba las unidades extraíbles que hay instaladas y mira a ver si alguna tiene el fichero gsCopia.txt. Fíjate que uso la función If para saber si debo añadir la barra del directorio o no. Si ninguna de las unidades extraíbles tiene ese fichero "especial", se usará la primera unidad extraíble que se haya encontrado (por hago la comprobación de que solo asigne el nombre de la unidad si la cadena está vacía).

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
        unidades = UtilDiscos.UnidadesExtraibles()

        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

    If hayExtraible Then
        ' Si hay unidad extraíble pero no coincide con la de destino
        If coinciden = False Then
            statusComprobando.Text = "El destino es " & _
                                     elExtraible & _
                                     " pero no hay extraíble con esa letra de unidad"
            statusComprobando.Visible = True
        Else
            statusComprobando.Visible = False
        End If
    Else
        statusComprobando.Text = "NO HAY UNIDAD EXTRAIBLE"
        statusComprobando.Visible = True
    End If

    ' El botón de copiar habilitarlo
    ' solo si hay extraíble y coincide con el destino
    btnCopiar.Enabled = coinciden

    yaEstoy = False
End Sub

Este método (hayUnidadExtraible) comprueba las unidades extraíbles que hay instaladas (hace una llamada al método UnidadesExtraibles de la clase UtilDiscos). También comprueba si la unidad de destino está entre las halladas, con idea de que si hay unidades extraíbles, pero no es la de destino, avise de ese hecho y deshabilite el botón de copiar.
En este mismo método se asigna el intervalo del temporizador (Timer), dando mayor margen cuando hay unidades extraíbles que cuando no las hay... no es que haga falta, pero...
La variable estática yaEstoy (Static) es para evitar que se entre en este mismo método si se está todavía dentro, esto puede ocurrir al principio, al iniciarse la aplicación, ya que el timer se activa en el evento Load del formulario y... bueno, que es una buena costumbre evitar que se entre en un método cuando ya se está dentro, particularmente si no queremos que eso ocurra... ya que en otros casos es posible que no nos importe que un método se ejecute nuevamente mientras aún se está ejecutando... pero bueno... tú haz lo que quieras, yo lo suelo hacer así, je, je.

Lo de la variable Static, en Visual Basic significa que esa variable mantiene el valor entre distintas llamadas al método. Que no es lo habitual, ya que cualquier variable usada en un método solo "dura" mientras dure el método, y cuando el método termina, se pierde el valor, pero con las variables Static de VB (esta característica es exclusiva de VB), ese valor no se pierde. Vamos es como si esa variable la definieras a nivel del formulario, que es en realidad lo que hace el compilador cuando genera el código IL.

Nota:
Cuando uses variables estáticas (Static) para esto que te acabo de comentar, es muy recomendable que compruebes que se ha asignado el valor que permita la reentrada (el valor False que asigno antes del End Sub), ya que si no te das cuenta de hacerlo, el método no volvería a ejecutarse. Este es importante sobre todo si usas algún Return o Exit Sub para salir del método, ya que si sales y no le asignas el valor falso, pues...

 

Private Sub habilitarControles(ByVal habilitar As Boolean)
    ' Llamada recursiva para habilitar adecuadamente
    ' todos los controles del formulario
    habilitarControles(Me.Controls, habilitar)

    ' Las excepciones que se deben considerar por separado

    ' Estas siempre deben estar deshabilitada
    chkComprobarDiscos.Enabled = False

    ' El botón de comprobar unidad extraíble no debe estar habilitado 
    ' si chkNoModificar.Checked = True
    If chkNoModificar.Checked Then
        btnComprobarDestino.Enabled = False
    End If

    ' El botón de copiar siempre debe estar habilitado
    ' (salvo que no haya unidad extraíble)
    hayUnidadExtraible()
End Sub

''' <summary>
''' Para habilitar recursivamente los controles
''' Si un control tiene controles, se hace una llamada recursiva
''' </summary>
''' <param name="controles">
''' La colección de controles a recorrer
''' </param>
''' <param name="habilitar">
''' Un valor True o False a asignar a los controles
''' </param>
''' <remarks></remarks>
Private Sub habilitarControles(ByVal controles As Control.ControlCollection, _
                               ByVal habilitar As Boolean)
    For Each ctrl As Control In controles
        If ctrl.Controls.Count > 0 Then
            habilitarControles(ctrl.Controls, habilitar)
        End If
        ctrl.Enabled = habilitar
    Next
End Sub

Estos dos métodos sirven para habilitar o deshabilitar los controles del formulario. La primera sobrecarga es en realidad la que se usará desde otros sitios del formulario, y desde ese primer método (que solo recibe un parámetro) se llama a la otra sobrecarga pasándole como segundo argumento la colección de controles a tener en cuenta.

Inicialmente le paso la colección Controls del formulario, y desde ese método, se llama a si mismo si el control en cuestión tiene a su vez otros controles. Por ejemplo, cuando llegue al GroupBox, comprobará los controles que contenga, etc.
Normalmente en la segunda sobrecarga habría que hacer algún tipo extra de comprobación, sobre todo si en el formulario hay menús, pero como en este formulario no hay menús, pues no tengo que hacer nada especial.

Como ves, hay ciertos controles que no deben cambiar de estado, por eso los trato de forma separada, pero una vez que ha regresado del método recursivo, con idea de que se asigne el valor que yo quiero después de haber hecho lo que tenga que hacer.

''' <summary>
''' Asignar los ficheros soltados
''' </summary>
''' <param name="cboDir"></param>
''' <param name="e"></param>
''' <remarks></remarks>
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 = String.Join(";", dirs)

    If String.IsNullOrEmpty(cboDir.Text) = False Then
        cboDir.Text &= ";" & sFic
    Else
        cboDir.Text = sFic
    End If
End Sub

''' <summary>
''' Ajustar el tamaño del formulario según se muestren los Expander
''' </summary>
''' <remarks></remarks>
Private Sub ajustarTamañoFormulario()
    Me.Height = 194 + expanderGroup.Height + expander2Group.Height
    statusStrip1.BringToFront()
End Sub

Estos dos métodos son los últimos métodos "no de evento" que tiene el formulario.

El primero (asignarFic) lo uso para asignar al combo indicado los ficheros que se hayan indicado en la operación de arrastrar y soltar (drag & drop). Fíjate que se hace la comprobación de si el texto ya tiene algún valor, con idea de agregar los nuevos valores soltados. Esos valores "múltiples" se indican separando cada valor con un punto y coma.

Por otro lado, el método ajustarTamañoFormulario lo que hace es cambiar el alto del formulario para los casos en que se han expandido o contraído los "expander", pues se quede más grande o más pequeño... en la página con los detalles de la aplicación puedes ver cómo queda el formulario según se muestren u oculten los "grupos" de controles con las opciones.

El valor "mágico" ese de 194 está calculado teniendo en cuenta el tamaño en modo de diseño, que como ves, lo que hago es sumar lo que ocupan los dos grupos de controles, con idea de que el tamaño tenga el alto adecuado.

 

Y ahora viene el código con los métodos de evento del formulario y los controles. Como ya te he comentado, en Visual Basic 2008 se permite definir un método de evento sin indicar los parámetros, esto es particularmente útil si esos parámetros no se van a usar, así que... si te extraña ver algunas de las definiciones de eventos, pues... ya sabes.

De todas formas, en los métodos de evento que si se van usar los parámetros, hay que dejar definidos los dos.

'
' Los métodos de eventos
'   Recuerda que en Visual Basic 2008 los parámetros de los eventos
'   no es necesario indicarlos si no se van a usar.
'   Si este código lo vas a usar para Visual Basic 2005 debes ponerle 
'   los parámetros que correspondan a estos métodos de eventos.
'


Private Sub Form1_FormClosing() Handles Me.FormClosing
    guardarCfg()
End Sub

Private Sub Form1_Load() Handles MyBase.Load
    progressBar1.Visible = False

    expanderSize = expanderGroup.Size
    expander2Size = expander2Group.Size

    ' El texto original de la cabecera
    expanderHeader.Tag = expanderHeader.Text
    expander2Header.Tag = expander2Header.Text

    leerCfg()

    ' Asignar el contrario a lo que se quiere conseguir
    ' ya que en el evento Click se cambia.
    expanderExpanded = Not expanderExpanded
    expanderPic_Click()
    expander2Expanded = Not expander2Expanded
    expander2Pic_Click()

    If DateTime.Now.Year > 2007 Then
        statusInfo.Text = "©Guillermo 'guille' Som, 2007-" & DateTime.Now.Year
    Else
        statusInfo.Text = "©Guillermo 'guille' Som, 2007"
    End If
    statusInfo.Tag = statusInfo.Text

    ' Activar el timer con un intervalo pequeño
    ' para que al cargar el formulario no tarde demasiado
    timerComprobarExtraibles.Interval = 100
    timerComprobarExtraibles.Enabled = True
End Sub

El primer método se produce cuando se cierra el formulario, y lo único que hago es guardar los valores de configuración.

El segundo es el evento Load del formulario en el que inicializo (asigno) el tamaño de cada uno de los dos GroupBox, con idea de que ese valor lo pueda volver a usar cuando se expandan los "expanders simulados".

Después leo los valores de la configuración y hago un "truco" para que se muestren u oculten los grupos de opciones según el valor que tengan las variables que controlan si deben estar esos grupos expandidos u ocultos. El truco es cambiar el valor que tienen las variables que controlan el estado de esos "expanders" y llamar al método Click de los PictureBox que, como verás en un momento, tienen en cuenta esas variables para saber cómo deben estar los "expanders".

Después asigno la fecha del copyright y guardo en la propiedad Tag del control statusInfo el texto actual, con idea de que lo pueda reponer cuando sea necesario.

Finalmente asigno un intervalo pequeños al temporizador y lo activo. El asignar un valor pequeño es con idea de que el timer ese se lance enseguida y se hagan los ajustes que haya que hacer (en realidad, desde el temporizador solo se llama al método que comprueba si hay unidades extraíbles).

'
' Al pulsar en las imágenes de los expander
'

Private Sub expanderPic_Click() Handles expanderPic.Click
    expanderExpanded = Not expanderExpanded

    If expanderExpanded Then
        expanderPic.Image = My.Resources.ExpanderUp
        expanderGroup.Size = expanderSize
        toolTip1.SetToolTip(expanderPic, " Ocultar las opciones ")
        expanderHeader.Text = expanderHeader.Tag.ToString
    Else
        expanderPic.Image = My.Resources.ExpanderDown
        expanderGroup.Size = expanderPanel.Size
        toolTip1.SetToolTip(expanderPic, " Mostrar las opciones ")
        expanderHeader.Text = expanderHeader.Tag.ToString & _
                                " (" & System.IO.Path.GetFileName(cboUtilidad.Text) & ")"
    End If

    ajustarTamañoFormulario()
End Sub

Private Sub expander2Pic_Click() Handles expander2Pic.Click
    expander2Expanded = Not expander2Expanded

    If expander2Expanded Then
        expander2Pic.Image = My.Resources.ExpanderUp
        expander2Group.Size = expander2Size
        toolTip1.SetToolTip(expander2Pic, " Ocultar las opciones ")
        expander2Header.Text = expander2Header.Tag.ToString
    Else
        expander2Pic.Image = My.Resources.ExpanderDown
        expander2Group.Size = expander2Panel.Size
        toolTip1.SetToolTip(expander2Pic, " Mostrar las opciones ")
    End If

    ajustarTamañoFormulario()
End Sub


''' <summary>
''' Comprobar si alguna unidad extraíble tiene el fichero gsCopia.txt
''' de ser así, se usará como destino la primera hallada
''' </summary>
''' <remarks></remarks>
Private Sub btnComprobarDestino_Click() Handles btnComprobarDestino.Click
    comprobarDestino()
End Sub

Voy a empezar por el último método, ya que lo único que hace es llamar al método que comprueba la unidad extraíble que tiene el fichero ese para saber qué unidad usar.

Los otros dos métodos son para cuando se pulsan en las imágenes con el "simbolico" ese para el expander. Aunque son dos métodos (uno para cada imagen) en realidad lo que hacen es lo mismo, pero aplicado a cada uno de los grupos en los que están esas imágenes. Lo primero que se hace es cambiar el valor de la variable que controla si se debe ocultar o mostrar, en caso de que después de esa asignación el valor que tiene esa variable es True (por tanto se cumplirá la primera comprobación), se asigna la imagen adecuada, se ajusta el tamaño que debe tener el GroupBox (que será el que tenía al iniciarse el programa cuando esté mostrado o bien será el tamaño del panel que contiene la imagen cuando esté oculto). También se ajusta el texto a mostrar en el caso de ser el grupo de arriba, con idea de que si está oculto, se sepa que utilidad se va a usar para la copia.
Finalmente se llama al método que ajusta el tamaño del formulario. 

''' <summary>
''' Copiar los datos usando las opciones indicadas
''' Se hace comprobación de que sean correctos (o casi)
''' </summary>
''' <remarks></remarks>
Private Sub btnCopiar_Click() Handles btnCopiar.Click
    Static yaEstoy As Boolean

    actualizarCombos()
    guardarCfg()

    ' Realizar una llamada a la utilidad indicada
    ' por cada valor que haya en origen
    ' (puede haber varios separados por punto y coma)

    Dim dest As String = My.Settings.Destino.Trim()
    If String.IsNullOrEmpty(dest) Then
        MessageBox.Show("Debes indicar la unidad de destino", _
                        "Copiar datos", _
                        MessageBoxButtons.OK, _
                        MessageBoxIcon.Exclamation)
        cboDestino.Focus()
        Exit Sub
    End If
    If dest.EndsWith("\") = False Then
        dest &= "\"
    End If
    If dest.EndsWith(":\") Then
        ' No permitir copiar en el raíz del destino
        MessageBox.Show("¡Atención! NO SE DEBE INDICAR EL RAÍZ DEL DESTINO." & vbCrLf & _
                        "Al menos indica un directorio, por ejemplo:" & vbCrLf & _
                        dest & "\COPIA", _
                        "Copiar datos", _
                        MessageBoxButtons.OK, _
                        MessageBoxIcon.Exclamation)
        cboDestino.Focus()
        Exit Sub
    End If

    Dim origen As String = My.Settings.Origen
    If String.IsNullOrEmpty(origen) Then
        MessageBox.Show("Debes indicar los datos de origen", _
                        "Copiar datos", _
                        MessageBoxButtons.OK, _
                        MessageBoxIcon.Exclamation)
        cboOrigen.Focus()
        Exit Sub
    End If
    Dim datosOrig() As String = origen.Split(";".ToCharArray, StringSplitOptions.RemoveEmptyEntries)

    Dim util As String = cboUtilidad.Text.ToLower()
    If String.IsNullOrEmpty(util) Then
        MessageBox.Show("Debes indicar la utilidad a usar para la copia", _
                        "Copiar datos", _
                        MessageBoxButtons.OK, _
                        MessageBoxIcon.Exclamation)
        If expanderExpanded = False Then
            expanderPic_Click()
        End If
        cboUtilidad.Focus()
        Exit Sub
    End If

    Dim params As String = cboParametros.Text
    If String.IsNullOrEmpty(params) Then
        MessageBox.Show("Debes indicar los parámetros a usar con la utilidad", _
                        "Copiar datos", _
                        MessageBoxButtons.OK, _
                        MessageBoxIcon.Exclamation)
        If expanderExpanded = False Then
            expanderPic_Click()
        End If
        cboParametros.Focus()
        Exit Sub
    End If

    If yaEstoy Then
        btnCopiar.Text = "Cancelando..."
        statusInfo.Text = "Cancelando..."
        cancelar = True
        Application.DoEvents()
        Exit Sub
    End If

    ' Se supone que los datos son correctos...

    habilitarControles(False)

    btnCopiar.Focus()
    btnCopiar.Text = "Cancelar"
    cancelar = False
    yaEstoy = True

    statusInfo.Text = "Copiando los datos..."
    Me.Cursor = Cursors.AppStarting
    Me.Refresh()

    With progressBar1
        .Style = ProgressBarStyle.Marquee
        .Minimum = 0
        .Maximum = 100
        .Value = 0
        .Visible = True
    End With

    For Each datoOri As String In datosOrig
        progressBar1.Value = 0

        Dim proceso As New Process
        With proceso
            Dim di As New DirectoryInfo(datoOri)

            ' Usar siempre el directorio de origen
            Dim dirDest As String = dest & If(dest.EndsWith("\"), "", "\") & di.Name
            Dim sb As New StringBuilder
            sb.AppendFormat("{0} {1} {2}", datoOri, dirDest, params)

            statusInfo.Text = "Copiando en " & dirDest & "..."

            'MessageBox.Show("Se va a ejecutar:" & vbCrLf & sb.ToString, "Probando")
            'Continue For

            .StartInfo.Arguments = sb.ToString
            .StartInfo.FileName = util
            If My.Settings.OcultarVentana Then
                .StartInfo.WindowStyle = ProcessWindowStyle.Minimized
            Else
                .StartInfo.WindowStyle = ProcessWindowStyle.Normal
            End If
            .StartInfo.WorkingDirectory = datoOri
            .Start()
            progressBar1.Value = 1
            Do
                Application.DoEvents()
                If cancelar Then
                    .Kill()
                    .Close()
                    Exit For
                End If
            Loop While .HasExited = False
        End With
    Next

    If cancelar = False AndAlso chkMostrarDestino.Checked Then
        ' Abrir la unidad de destino
        ' (si se indica en la opción correspondiente y no se ha cancelado)
        Process.Start("explorer.exe", dest)
    End If

    Me.Cursor = Cursors.Default
    progressBar1.Visible = False
    statusInfo.Text = statusInfo.Tag.ToString

    btnCopiar.Text = "Copiar"

    habilitarControles(True)

    yaEstoy = False
End Sub

Este "peazo" método es el que se usará cuando se pulse en el botón de copiar.
Lo primero que se hace es actualizar los combos y guardar los datos en la configuración (Settings).
Después se hacen varias comprobaciones, como es que se indique dónde copiar y qué copiar, y en el caso de la unidad de destino sea el directorio raíz, pues avisarlo y salir.
También se crea un array de tipo String con cada uno de los valores que haya en origen, (recuerda que se pueden indicar varios valores separados por puntos y comas).

Una vez hechas las comprobaciones de los valores a usar para la copia, se comprueba si la variable estática yaEstoy tiene un valor True, en ese caso, significa que es la segunda vez que se pulsa en el botón, lo que quiere decir que se quiere cancelar, por tanto, se muestra el texto adecuado, se asigna un valor True a la variable cancelar y se permite a Windows que procese todos los mensajes que tenga pendiente, con idea de que ese valor de cancelación llegue al método que actualmente se estará ejecutando (que es este mismo, ya que si yaEstoy vale True es que aún se está ejecutando este método), como dejamos de la mano de la comprobación que se haga del valor de la variable cancelar el que en realidad se cancele, pues salimos del método. (Ahora veremos cuando se entera el código que se quiere cancelar.)

Si no se ha cancelado, es decir, es la primera vez que se entra en este método, se deshabilitan los controles con idea de que el usuario pulse en algunos de los controles y cambie los valores que hay, ya que si entramos en una operación que puede ser larga, lo mejor es no permitir que los usuarios interactúen con la aplicación, salvo que así lo tengamos previsto.
Después cambiamos el texto del botón para que indique que se puede cancelar, además de asignar los valores a algunas de las variables (la más importante es la asignación de True a yaEstoy, que es la que detectará que se quiere cancelar, tal como te he explicado hace un momento).

Lo siguiente es recorrer cada uno de los valores del array con los datos que se quieren copiar, creamos un objeto del tipo Process (que será el que usaremos para lanzar la utilidad de copia), con idea de que se use el mismo directorio en el destino que el indicado en el origen, lo que hago es usar el nombre del directorio de origen (que será el último nombre de directorio indicado en origen) y usar ese nombre para que se use en el destino.

¿No te has enterado?
A ver, te lo explico, supón que el origen es C:\Datos\A2007 y el destino es F:\Copia, pues se usará como destino F:\Copia\A2007, es decir, se usa el último directorio del origen y se agrega al valor del destino.

Después se asignan los parámetros a la clase Process y se inicia el proceso... y como debemos esperar hasta que se termine, pues lo hacemos en un bucle Do... Loop en el que se comprueba que la propiedad HasExited del proceso sea False, (es decir, que no haya terminado), en cuyo caso, se continua el bucle. Si estando dentro del bucle de espera, se detecta que el valor de la variable cancelar es verdadero, quiere decir que hay que cancelar lo que se está haciendo, por tanto, se finaliza el proceso que habíamos iniciado y salimos del bucle For.

Cuando el bucle For termina (bien porque se haya cancelado o porque se haya terminado de hacer las copias), si no se ha cancelado y tampoco se ha indicado que no se haga, pues... se abre la ventana del disco de destino usando una llamada a la aplicación explorer.exe que es la que hace que veas las carpetas en Windows.

Finalmente se asignan los valores "normales" a las variables de estado (las usadas para saber que estábamos en ese método, etc.) y habilitamos nuevamente los controles.

'
' Los botones de selección
'

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
        '.SelectedPath = Me.cboDestino.Text
        If .ShowDialog = System.Windows.Forms.DialogResult.OK Then
            Me.cboDestino.Text = .SelectedPath
        End If
    End With
End Sub

Private Sub btnSelecUtil_Click() Handles btnSelecUtil.Click
    Dim oFD As New OpenFileDialog
    With oFD
        .Title = "Seleccionar la utilidad para la copia"
        .Filter = "Ejecutables (*.exe; *.bat; *.cmd)|*.exe; *.bat; *.cmd|Todos (*.*)|*.*"
        .FileName = cboUtilidad.Text
        .CheckFileExists = True
        .CheckPathExists = True
        If .ShowDialog = System.Windows.Forms.DialogResult.OK Then
            cboUtilidad.Text = .FileName
        End If
    End With
End Sub

Private Sub btnSeleccionarOri_Click() Handles btnSeleccionarOri.Click
    Dim oFD As New System.Windows.Forms.FolderBrowserDialog
    With oFD
        .Description = "Seleccionar el directorio de origen"
        .RootFolder = Environment.SpecialFolder.MyComputer
        ' Si el texto tiene varios directorios,
        ' usar solo el primero
        If Me.cboOrigen.Text.Contains(";") Then
            Dim i As Integer = Me.cboOrigen.Text.IndexOf(";")
            .SelectedPath = Me.cboOrigen.Text.Substring(0, i)
        Else
            .SelectedPath = Me.cboOrigen.Text
        End If
        If .ShowDialog = System.Windows.Forms.DialogResult.OK Then
            ' Si ya había un texto en el combo, agregarlo al final
            If String.IsNullOrEmpty(Me.cboOrigen.Text) = False Then
                Me.cboOrigen.Text &= "; " & .SelectedPath
            Else
                Me.cboOrigen.Text = .SelectedPath
            End If
        End If
    End With
End Sub

Estos tres métodos son los de los tres botones de selección. El primero es para seleccionar el directorio de destino, el segundo es para seleccionar una utilidad para realizar la copia y el último es para seleccionar el directorio de origen. En este último, debido a que se permite múltiple selección, pues se tiene en cuenta si ya había algo antes, en cuyo caso se añade usando un punto y coma como separador. Y si hay varios directorios cuando se pulsa en ese botón, se usa como predeterminado el primero de ellos.

'
' Para las opciones de arrastrar y soltar
'

Private Sub Form1_DragEnter(ByVal sender As Object, _
                            ByVal e As System.Windows.Forms.DragEventArgs) _
                            Handles Me.DragEnter, _
                                    cboOrigen.DragEnter, cboDestino.DragEnter, cboUtilidad.DragEnter
    ' Drag & Drop, comprobar con DataFormats
    If e.Data.GetDataPresent(DataFormats.FileDrop) Then
        e.Effect = DragDropEffects.Copy
    End If
End Sub

Private Sub cboOrigen_DragDrop(ByVal sender As Object, _
                               ByVal e As System.Windows.Forms.DragEventArgs) _
                               Handles cboOrigen.DragDrop, MyBase.DragDrop
    If e.Data.GetDataPresent("FileDrop") Then
        asignarFic(cboOrigen, e)
    End If
End Sub

Private Sub cboDestino_DragDrop(ByVal sender As Object, _
                                ByVal e As System.Windows.Forms.DragEventArgs) _
                                Handles cboDestino.DragDrop
    If e.Data.GetDataPresent("FileDrop") Then
        asignarFic(cboDestino, e)
    End If
End Sub

Private Sub cboUtilidad_DragDrop(ByVal sender As Object, _
                                 ByVal e As System.Windows.Forms.DragEventArgs) _
                                 Handles cboUtilidad.DragDrop
    If e.Data.GetDataPresent("FileDrop") Then
        cboUtilidad.Text = CType(e.Data.GetData("FileDrop", True), String())(0)
    End If
End Sub

Todo este código es el usado para las operaciones de arrastrar y soltar. Lo primero es comprobar si se está arrastrando algo sobre los controles que queremos usar con esas opciones, y de ser así, se asigna un valor adecuado a la propiedad Effect de la variable usada como segundo parámetro del método.
En los dos siguientes, se usa el método asignarFic para asignar el fichero (o ficheros, aunque en realidad deberían ser directorios). Mientras que en el último, lo que se hace es asignar el primer fichero que se haya soltado, ya que el destino de esa operación Drag & Drop es el combo de la utilidad para la copia.

'
' Al cambiar los checkBox, hacer las comprobaciones que correspondan
'

Private Sub chkNoSeleccionar_CheckedChanged() Handles chkNoSeleccionar.CheckedChanged
    Static yaEstoy As Boolean
    ' No permitir la reentrada, ya que desde aqui se puede cambiar el valor
    If yaEstoy Then Exit Sub

    yaEstoy = True

    If chkNoModificar.Checked Then
        chkNoSeleccionar.Checked = True
    End If
    btnSeleccionarDes.Enabled = Not chkNoSeleccionar.Checked
    btnSeleccionarOri.Enabled = btnSeleccionarDes.Enabled
    btnSelecUtil.Enabled = btnSeleccionarDes.Enabled

    yaEstoy = False
End Sub

Private Sub chkNoModificar_CheckedChanged() Handles chkNoModificar.CheckedChanged
    If chkNoModificar.Checked Then
        ' Asegurarse de que el texto actual ya esté en la lista
        ' con idea de usarlo para seleccionar el elemento a mostrar
        actualizarCombos()
        guardarCfg()

        cboDestino.DropDownStyle = ComboBoxStyle.DropDownList
        cboOrigen.DropDownStyle = cboDestino.DropDownStyle
        cboUtilidad.DropDownStyle = cboDestino.DropDownStyle
        cboParametros.DropDownStyle = cboDestino.DropDownStyle

        cboDestino.Text = My.Settings.Destino
        cboOrigen.Text = My.Settings.Origen
        cboUtilidad.Text = My.Settings.Utilidad
        cboParametros.Text = My.Settings.Parametros

        btnComprobarDestino.Enabled = False

        ' Si no se puede escribir, no permitir la selección
        ' de directorios (ni de la utilidad de copia, etc.)
        chkNoSeleccionar.Checked = True
        chkNoSeleccionar.Enabled = False
    Else
        cboDestino.DropDownStyle = ComboBoxStyle.DropDown
        cboOrigen.DropDownStyle = cboDestino.DropDownStyle
        cboUtilidad.DropDownStyle = cboDestino.DropDownStyle
        cboParametros.DropDownStyle = cboDestino.DropDownStyle

        btnComprobarDestino.Enabled = True
        chkNoSeleccionar.Enabled = True
    End If
End Sub

Estos dos métodos son para los dos CheckBox de la configuración, el primero es el usando para indicar si se puede o no seleccionar (por medio de los botones Seleccionar), y el segundo es para evitar que se modifique el texto de los combos (acuérdate de todo lo que te expliqué hace un rato).

En el primer método uso una variable estática para evitar la "famosa" reentrada mientras se está haciendo algo. Y esto es necesario porque desde ese mismo método se asigna un valor verdadero al CheckBox que ha producido el evento (lo que haría que se volviera a producir ese mismo evento otra vez). Esa asignación se hace porque si el otro CheckBox (el de impedir que se escriba) está marcado, pues... este también debería estarlo... (ya te lo explique hace un rato).
En cualquier caso, no es necesario hacer esto, ya que si la otra opción está seleccionada, ésta no estará disponible... pero bueno, así lo tengo... y así lo dejo... (Es que en un principio dejé de forma independiente ambas opciones, pero después pensé que no tenía razón de ser lo de seleccionar algo cuando lo que se seleccione no se puede asignar a ningún lado, así que...)

Independientemente de la "relación" que hay entre estas dos opciones, en cada método se habilitan o deshabilitan adecuadamente los controles que están relacionados, y en el caso del segundo método, lo que se hace es cambiar el estilo de los combos para que no se pueda escribir en ellos o sí, según como esté esa opción.
Lo importante aquí es que antes de cambiar el estilo de los combos a DropDownList, hay que asignar los combos y guardar los datos de configuración, con idea de que el texto que se va a asignar exista en los elementos, porque, (tal como te dije más arriba), si está en ese estilo, el texto que se asigne solo tendrá efecto si forma parte de los elementos de la lista.

    Private Sub timerComprobarExtraibles_Tick() Handles timerComprobarExtraibles.Tick
        ' Comprobar si ya hay discos extraíbles disponibles
        timerComprobarExtraibles.Enabled = False
        hayUnidadExtraible()
        timerComprobarExtraibles.Enabled = True
    End Sub

    Private Sub btnCfgListas_Click() Handles btnCfgListas.Click
        actualizarCombos()
        guardarCfg()

        If My.Forms.fConfigListas.ShowDialog() = Windows.Forms.DialogResult.OK Then
            leerCfg()
        End If
    End Sub

    Private Sub btnSalir_Click() Handles btnSalir.Click
        Me.Close()
    End Sub
End Class

Y ya llegamos al final del código del formulario principal. Lo que tenemos aquí es el evento del temporizador (timer) en el que lo único que se hace es desactivar el temporizador, llamar al método de comprobación de las unidades extraíbles y después volver a activarlo.

Cuando se pulsa en el botón de configuración de las listas, lo que tenemos que hacer es actualizar los combos (ya sabes, para que se guarden los textos que no estén en los elementos de los combos), guardar los datos en la configuración y después mostrar el formulario de configuración.
Si se pulsa en Aceptar (del formulario de configuración) se leen los datos de la configuración y de esa forma se actualizarán los cambios que se hayan hecho en ese cuadro de diálogo. Ya que, como verás más abajo, dentro de ese formulario se guardan los datos de la configuración con los cambios que se hayan hecho, de esa forma estarán perfectamente "sincronizados", por decirlo de alguna forma, vamos que así es más fácil transferir los datos que se deben tener en cuenta, ya que los valores de My.Settings están disponibles en toda la aplicación, y si no existieran, pues habría que asignar los valores al llamar al formulario de configuración y cuando se volviera, habría que obtener los nuevos datos. Si no has "sufrido" las versiones anteriores de Visual Basic lo mismo no te dice nada todo esto, pero los que las hayan usado, pues seguramente entenderán de que estoy hablando... en cualquier caso, no te preocupes, que algunas veces me da por desvariar, je, je.

El último método lo que hace es cerrar el formulario principal, con lo que se termina la aplicación.

 

El formulario de configuración del contenido de los combos

Como ya te comenté antes (o en la página de los detalles de gsCopia) este formulario se usa para modificar el contenido de las listas de elementos de los combos. Y como te he comentado en el párrafo anterior, los datos del contenido de esas listas los sabemos por medio de las propiedades de configuración. Así que... los cambios que se hagan en este formulario en realidad se harán en los datos de configuración, pero como veremos, en cualquier caso se puede deshacer lo que se haga, ya que solo se actualizarán esos datos de configuración cuando se pulse en el botón aceptar del formulario.

Veamos el código y un poco de explicación del código usado, ya que en este formulario uso unos "truquillos" para relacionar los controles de cada grupo de controles, ahora veremos cómo.

'------------------------------------------------------------------------------
' fConfigListas                                                     (12/Dic/07)
' Para configurar el contenido de las listas (combos) de la utilidad gsCopia
'
' ©Guillermo 'guille' Som, 2007
'------------------------------------------------------------------------------
Option Strict On
'Option Infer Off

Imports Microsoft.VisualBasic
Imports vb = Microsoft.VisualBasic
Imports System
Imports System.Windows.Forms
Imports System.Drawing

'Imports System.IO
Imports System.Collections.Generic
'Imports System.Text
'Imports System.Diagnostics

Public Class fConfigListas

    Private expanded As New List(Of Boolean)
    Private grSize As New List(Of Size)
    Private lvListas As New List(Of ListView)

El principio del código son las importaciones de espacios de nombres.

Nota:
Si te preguntas porqué indico esas importaciones de forma explícita, pues... decirte que es porque tengo esa costumbre, ya que como sabes en Visual Basic los espacios de nombres "habituales" suelen estar importados automáticamente, pero a mi me gusta indicarlos de forma explícita... costumbres... más que nada para que así se sepa qué espacios de nombres estoy usando en cada fichero, además de que a la hora de convertir el código a C#, pues es más fácil.

En este formulario la forma de manejar los "expanders" y grupos de opciones lo he hecho de forma un poco diferente al código que te mostré antes, pero básicamente (como podrás comprobar) en ambos casos hago lo mismo, lo que cambia es que en este formulario voy a usar unos arrays (mejor dicho unas listas generic) para guardar el estado de expandido o contraído de los grupos, lo mismo para el tamaño inicial de los GroupBox y en el caso de las listas (ListView), también los guardo en una colección generic de tipo List. Ahora verás cómo asignar los valores y cómo recuperarlos.

Public Sub New()

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

    ' Add any initialization after the InitializeComponent() call.

    ' Líneas 3D
    Dim ColorOscuro As System.Drawing.Color = Color.FromKnownColor(KnownColor.ControlDark)
    Dim ColorClaro As System.Drawing.Color = Color.FromKnownColor(KnownColor.ControlLightLight)
    Const indentValue As Integer = 4
    Dim mWidth As Integer = Me.DisplayRectangle.Width - (indentValue * 2)

    With linea1
        .BackColor = ColorOscuro
        .Height = 1
        .Left = indentValue
        .Width = mWidth
    End With
    With linea2
        .BackColor = ColorClaro
        .Height = 1
        .Top = linea1.Top + 1
        .Left = indentValue
        .Width = mWidth
    End With

    ' Crear las listas usando los valores indicados
    expanded = crearLista(True, True, True, True)
    grSize = crearLista(GroupBox1.Size, GroupBox2.Size, GroupBox3.Size, GroupBox4.Size)
    lvListas = crearLista(listView1, listView2, listView3, listView4)

    For Each lv As ListView In lvListas
        lv.Columns(0).Width = lv.ClientRectangle.Width - 12
    Next

    btnDeshacer_Click()
End Sub

Private Function crearLista(Of T)(ByVal ParamArray valores() As T) As List(Of T)
    Dim lista As New List(Of T)
    lista.AddRange(valores)

    Return lista
End Function

En el constructor del formulario (lo podría haber hecho también en el evento Load) asigno los valores para las dos etiquetas que harán el efecto 3D de separación.
Después asigno los valores iniciales a las tres colecciones que vimos antes.
Para hacer esas asignaciones uso la función crearLista, que como puedes comprobar es de tipo generic, recibe un array de valores opcionales del tipo indicado y devuelve una colección del tipo List, pero del mismo tipo usado como argumento.
Asigno un valor True a cada una de las variables de los expanders, ya que inicialmente se muestra expandidos.
A la colección gsSize (la que contendrá el tamaño inicial de cada GroupBox) le asigno el tamaño de cada uno de los cuatro GroupBox usados en este formulario.
Por último asigno los cuatro ListView. A esos ListView le asigno el tamaño de la única columna que tienen, de forma que tengan el tamaño completo del ancho, menos un pequeño espacio por si se debe mostrar el scroll vertical.
Y antes de terminar el código del constructor, llamo al evento Click del botón deshacer, que como verás ahora, es el que se encarga de asignar los valores a las listas.

El método ese es un "truco" para asignar valores a una colección, ya que Visual Basic 2008 no tiene la posibilidad de crear una colección y asignarle los valores, pero con ese método, pues... es fácil hacerlo.
El "T" ese que se usa es para indicar el tipo de datos que se va a manejar en esa función, aunque en realidad lo único que se hace es crear una colección del tipo adecuado (T) y asignarle los datos a esa colección y después devolverla. Puede que sea una manera un poco rebuscada, pero... una vez que tienes esa función creada, pues... resulta más fácil y cómodo crear las colecciones con los valores... o al menos a mi así me lo parece... que sobre gustos, pues ya sabe que no hay nada escrito, je, je, je.

Private Sub pic1_Click(ByVal sender As Object, _
                       ByVal e As EventArgs) _
                       Handles pic1.Click, pic2.Click, pic3.Click, pic4.Click
    ' El picture en el que se ha hecho click
    Dim pic As PictureBox = TryCast(sender, PictureBox)
    If pic Is Nothing Then Exit Sub
    ' El panel en el que está este picture
    Dim expanderPanel As Panel = TryCast(pic.Parent, Panel)
    If expanderPanel Is Nothing Then Exit Sub
    ' El GroupBox en el que está el picture
    Dim expanderGroup As GroupBox = TryCast(expanderPanel.Parent, GroupBox)
    If expanderGroup Is Nothing Then Exit Sub

    ' El índice será el final del nombre menos uno
    ' (los pictures deben llamarse nnnn1, nnnn2, etc.)
    Dim index As Integer = CInt(vb.Right(pic.Name, 1)) - 1

    Dim expanderExpanded As Boolean = expanded(index)
    Dim expanderSize As Size = grSize(index)

    expanderExpanded = Not expanderExpanded

    If expanderExpanded Then
        pic.Image = My.Resources.ExpanderUp
        expanderGroup.Size = expanderSize
        toolTip1.SetToolTip(pic, " Ocultar las opciones ")
    Else
        pic.Image = My.Resources.ExpanderDown
        expanderGroup.Size = expanderPanel.Size
        toolTip1.SetToolTip(pic, " Mostrar las opciones ")
    End If

    expanded(index) = expanderExpanded

    ' Ajustar el tamaño del formulario
    Me.Height = 110 + GroupBox1.Height + GroupBox2.Height + GroupBox3.Height + GroupBox4.Height

End Sub

Este método es el que se encarga de gestionar el evento Click de las imágenes. Como puedes apreciar el código es muy parecido al que ya vimos en el formulario principal, pero en este caso, el mismo método evento vale para todos los PictureBox.

Lo primero es hacer un "cast" (conversión) del primer parámetro al tipo PictureBox, para ello uso TryCast, que tiene la peculiaridad de que si no se puede hacer esa conversión, lo que devuelve es un valor Nothing, pero no produce ningún error (ese error se produciría si a este método se lo llamará desde otro control que no sea un PictureBox).
Como esa imagen está contenida en un panel, se asigna a la variable expanderPanel el valor del "Parent" de la imagen. El panel lo necesitamos para saber el tamaño del GroupBox cuando no está expandido.
Y como resulta que ese panel está a su vez contenido en un GroupBox, pues usamos el Patente del panel para averiguar qué GroupBox estamos usando.

Después tenemos que averiguar el índice de las colecciones que tenemos que usar.
En este caso, el "truco" está en la forma en que he llamado a los PictureBox, ya que le he dado nombres terminados en un número, con idea de usar ese número para saber en qué grupo estamos, y eso es lo que hago al asignar la variable index, en la que asigno el valor del último carácter del nombre al que le resto uno, ya que en los arrays y colecciones de .NET, el primer elemento es el que está en el índice cero.

Lo que hago a continuación es prácticamente lo mismo que te expliqué en el formulario anterior. Aunque en este caso, el cambio del tamaño del formulario lo hago al final de ese método, ya que ahí se tiene ya toda la información que necesitamos para asignar el alto del formulario. Nuevamente uno un número "mágico" que es el valor que tengo calculado para que al sumárselo a los valores de la altura de los GroupoBox, pues... pueda saber el tamaño adecuado del formulario.

Private Sub btnCancelar_Click() Handles btnCancelar.Click
    Me.DialogResult = Windows.Forms.DialogResult.Cancel
End Sub

Private Sub btnAceptar_Click() Handles btnAceptar.Click
    With My.Settings
        .OrigenCol = items2Str(listView1)
        .DestinoCol = items2Str(listView2)
        .UtilidadCol = items2Str(listView3)
        .ParametrosCol = items2Str(listView4)
    End With
    Me.DialogResult = Windows.Forms.DialogResult.OK
End Sub

En el evento del botón Cancelar, lo que hacemos es asignar el valor DialogResult.Cancel a la propiedad DialogResult del formulario, y esa asignación se encargará de cerrar (u ocultar) el formulario y devolver ese valor por medio del método ShowDialog que fue el usado para mostrar esta ventana.

Si se pulsa en Aceptar, lo que hacemos es asignar a las propiedades de la configuración (Settings) el contenido de los ListView. Para convertir ese contenido en una cadena, uso el método items2Str que es muy parecido al que ya vimos antes (cbo2String).
Finalmente se asigna un valor OK a la propiedad DialogResult para que el método ShowDialog devuelva ese valor y el otro formulario sepa que se han cambiado los datos de los combos.

Private Sub btnElimnar1_Click(ByVal sender As Object, _
                              ByVal e As EventArgs) _
                              Handles btnElimnar1.Click, btnElimnar2.Click, _
                                      btnElimnar3.Click, btnElimnar4.Click
    Dim btn As Button = TryCast(sender, Button)
    If btn Is Nothing Then Exit Sub

    ' El índice será el final del nombre menos uno
    Dim index As Integer = CInt(vb.Right(btn.Name, 1)) - 1

    eliminarSeleccionados(lvListas(index))

End Sub

Private Sub listView1_KeyUp(ByVal sender As Object, _
                            ByVal e As KeyEventArgs) _
                            Handles listView1.KeyUp, listView2.KeyUp, _
                                    listView3.KeyUp, listView4.KeyUp
    If e.KeyCode = Keys.Delete Then
        Dim lv As ListView = TryCast(sender, ListView)
        If lv Is Nothing Then Exit Sub

        eliminarSeleccionados(lv)
    End If
End Sub

''' <summary>
''' Elimnar los elementos seleccionados del listView indicado
''' </summary>
''' <param name="lv"></param>
''' <remarks></remarks>
Private Sub eliminarSeleccionados(ByVal lv As ListView)
    With lv
        Dim total As Integer = .SelectedIndices.Count
        If total > 0 Then
            For i As Integer = .SelectedIndices.Count - 1 To 0 Step -1
                .Items.RemoveAt(.SelectedIndices.Item(i))
            Next
            If total <> .SelectedIndices.Count Then
                btnDeshacer.Enabled = True
                btnAceptar.Enabled = True
            End If
        End If
    End With
End Sub

Este es el código usado para eliminar elementos de los ListView, en el primero caso se hace por medio de los botones Eliminar, que para saber cuál es el ListView que hay relacionado, lo que hago es usar el mismo truco del índice tomado del número en que acaba el nombre de cada control (cómo hecho de menos los arrays de controles de VB6, que hacía todo esto mucho más fácil, en fin... pero esto es lo que hay, así que...)

El código no necesita más explicación (creo) ya que prácticamente esto es lo mismo que se hizo antes. En cuanto al método que borra los elementos, fíjate que el bucle se hace al revés, desde el último al primero, con idea de que no intentemos acceder a un elemento que ya hemos eliminado. Después, si se han borrado elementos, se habilitan los dos botones con idea de que se pueda deshacer lo borrado o se pueda aceptar esos cambios.

    Private Sub btnDeshacer_Click() Handles btnDeshacer.Click
        ' Dejar los valores iniciales
        With My.Settings
            str2ListView(.OrigenCol, listView1)
            str2ListView(.DestinoCol, listView2)
            str2ListView(.UtilidadCol, listView3)
            str2ListView(.ParametrosCol, listView4)
        End With
        btnDeshacer.Enabled = False
        btnAceptar.Enabled = False
    End Sub

    Private Function items2Str(ByVal cbo As ListView) As String
        Dim lista As New List(Of String)
        For Each s As ListViewItem In cbo.Items
            lista.Add(s.Text)
        Next
        Return String.Join("|", lista.ToArray())
    End Function

    Private Sub str2ListView(ByVal datos As String, ByVal lv As ListView)
        Dim ar() As String = datos.Split("|".ToCharArray, _
                                         StringSplitOptions.RemoveEmptyEntries)
        lv.Items.Clear()
        For Each s As String In ar
            lv.Items.Add(s)
        Next
    End Sub

d End Class

Por último tenemos el código asociado con el botón de deshacer y las dos funciones que convierten los elementos en una cadena y viceversa.

Al deshacer, lo que se hace es recuperar los valores de la configuración y por medio del método str2ListView asignar esa cadena en elementos del ListView usado como segundo argumento. Después se deshabilitan los dos botones, ya que si se deshace, es que no hay cambios, así que... tampoco hay nada que aceptar ni volver a deshacer.

Si necesitas explicación para los dos últimos métodos, quiere decir que no te has leído lo que expliqué antes, así que... vuelve a leer todo, je, je.

 

Y esto es todo sobre los formularios y clases usadas en esta utilidad.

 

Confío que con todas estas explicaciones te haya quedado claro que es lo que hace el código, pero en realidad la "gracia" de estas explicaciones ha sido contarte los trucos y "técnicas" usadas para hacer lo que había que hacer. Y como ves, ese código lo podrás re-usar en otras aplicaciones con pocos cambios. Por ejemplo, ya sabes cómo crear un "simulador" del Expander de WPF y, espero, algunas cosas más, je, je

 

 

Espero que todo esto te sea de utilidad.

Nos vemos.
Guillermo


Código de ejemplo (comprimido):

Los links al ejecutable y el proyecto para Visual Basic 2008 está en las páginas que te he indicado al principio de esta página. 


 


La fecha/hora en el servidor es: 23/11/2024 0:44:13

La fecha actual GMT (UTC) es: 

©Guillermo 'guille' Som, 1996-2024