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