Introducción:
En esta revisión de la utilidad gsCopia, lo que he hecho es permitir
indicar unidades de disco fijo como destino, aunque si esa unidad es la que
se ha usado como arranque no se permite que se indique.
Lo de no permitir que se indique como destino la unidad usada por el
sistema, es porque en Windows Vista no se permite guardar cosas en esa
unidad, salvo que lo indiques expresamente tras un aviso de seguridad, así
que... para evitar problemas he excluido de la búsqueda de unidades la que
contenga el directorio del sistema.
Más abajo te explico como se hacen esas averiguaciones y comprobaciones.
Aquí tienes el código con los cambios que he hecho en esta revisión 1.0.3.0 y
más abajo tienes el resto de links de las revisiones anteriores, la explicación detallada y
el link para instalarlo con ClickOnce.
Empecemos viendo una captura de la aplicación en ejecución en la que
veremos un par de cosas nuevas:
Figura 1. La utilidad en ejecución (versión 1.0.3.0)
Como podemos ver en la figura 1, en las opciones de configuración tenemos
una nueva opción: Permitir usar destinos no extraíbles, esa
opción inicialmente tendrá un valor falso con idea de compatibilizar con los
valores de las versiones anteriores de la utilidad, pero si la seleccionamos
nos permitirá indicar como destino una unidad que no sea extraíble.
Si la unidad de destino no es extraíble, se avisa en la barra de estado,
pero con color verde, para que no se vea como un error, solo para que se
sepa que no se está usando una unidad extraíble.
Como te comentaba antes, ahora no se permite usar el disco de arranque
como destino, por tanto, se hacen las comprobaciones correspondientes tanto
en el código de arrastrar y soltar como en el de seleccionar, pero solo si
el destino de esa selección (o de los ficheros soltados) es el combo de la
unidad de destino.
Incluso al arrastrar al destino, si se está arrastrando algo de la unidad
de arranque, se mostrará el cursor de prohibido, tal como puedes ver en la
figura 2:
Figura 2. No se permite soltar cosas de la unidad de arranque
Lo que si se permite es indicar la unidad de arranque en el origen. En el
caso de la figura 2, mi disco de arranque es el disco C.
Tanto en la figura 1 o la 2 puedes ver que ahora se muestran también los
parámetros de la utilidad de copia, al menos si el contenido de ese grupo
está contraído.
Veamos ahora el código que ha cambiado con respecto a
la revisión de ayer.
Recuerda que el código que te muestro es solo el
nuevo, el completo lo tienes en el ZIP
con el proyecto o bien si vas uniendo el que he ido mostrando en
las revisiones anteriores.
El nuevo código de la versión 1.0.3.0
Empecemos viendo el código del módulo UtilDiscos:
Imports vb = Microsoft.VisualBasic
Lo primero es agregar un alias al espacio de nombres de
Microsoft.VisualBasic, con idea de usar las funciones propias de VB
por medio de ese "alias", (en realidad solo se usa la función Len).
''' <summary>
''' Si se debe incluir la unidad de arranque al llamar a UnidadesExtraibles
''' (por defecto NO se incluye)
''' </summary>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public IncluirUnidadBoot As Boolean = False
''' <summary>
''' Comprueba si el path indicado está en la unidad de arranque
''' </summary>
''' <param name="pathAComprobar">
''' La ruta que se comprobará si está en el disco de arranque
''' </param>
''' <returns>
''' Un valor verdadero o falso según sea
''' la unidad del path indicado el disco de arranque
''' </returns>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public Function EsBoot(ByVal pathAComprobar As String) As Boolean
' Si se indica un path vacío, se comprueba el directorio actual
If vb.Len(pathAComprobar) = 0 Then
pathAComprobar = Environment.CurrentDirectory
End If
If String.Compare(pathAComprobar.ToLower().Substring(0, 1), _
UnidadBoot.ToLower().Substring(0, 1)) = 0 Then
Return True
End If
Return False
End Function
''' <summary>
''' El nombre de la unidad de arranque
''' (se asigna en el constructor)
''' </summary>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public ReadOnly UnidadBoot As String = "C:\"
''' <summary>
''' Constructor para saber cuál es la unidad de arranque
''' </summary>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Sub New()
' Averiguar cuál es la unidad del sistema
Try
UnidadBoot = System.IO.Path.GetPathRoot(Environment.SystemDirectory)
Dim di As New System.IO.DirectoryInfo(Environment.SystemDirectory)
UnidadBoot = di.Root.FullName
Catch ex As Exception
' Si da error, usar el root del directorio del sistema
UnidadBoot = System.IO.Path.GetPathRoot(Environment.SystemDirectory)
End Try
End Sub
''' <summary>
''' Comprobar si la unidad del path indicado es extraíble
''' </summary>
''' <param name="pathAComprobar">
''' La ruta en la que se comprobará si es una unidad extraíble
''' </param>
''' <returns></returns>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public Function EsExtraible(ByVal pathAComprobar As String) As Boolean
' Si se indica un path vacío, se usa el directorio actual
If vb.Len(pathAComprobar) = 0 Then
pathAComprobar = Environment.CurrentDirectory
End If
Dim retType As TipoUnidades
Try
retType = GetDriveType(pathAComprobar)
Catch ex As Exception
retType = TipoUnidades.Desconocido
End Try
Return (retType = TipoUnidades.Extraible)
End Function
''' <summary>
''' Devuelve un array con las unidades extraibles
''' (en teoría para las llaves USB)
''' El nombre de cada unidad incluye la barra final,
''' por ejemplo F:\
''' </summary>
''' <returns>
''' Devuelve un array con las unidades extraíbles
''' </returns>
''' <remarks></remarks>
Public Function UnidadesExtraibles() As String()
Return UnidadesExtraibles(False)
End Function
''' <summary>
''' Devuelve un array con las unidades extraíbles y opcionalmente los fijos
''' El nombre de cada unidad incluye la barra final,
''' por ejemplo F:\
''' </summary>
''' <param name="incluirFijos">
''' Un valor verdadero para incluir las unidades fijas
''' </param>
''' <returns>
''' Devuelve un array con las letras de las unidades
''' que coinciden con lo indicado
''' </returns>
''' <remarks>
''' 16/Dic/07
''' </remarks>
Public Function UnidadesExtraibles(ByVal incluirFijos As Boolean) As String()
Dim extraibles As New List(Of String)
Dim drives() As String = Environment.GetLogicalDrives()
' Solo comprobar si no se debe incluir
' (es decir, IncluirUnidadBoot sea False)
Dim comprobadoBoot As Boolean = IncluirUnidadBoot
For Each s As String In drives
' Si no se debe incluir la unidad de arranque
' (y no se ha comprobado ya)
If comprobadoBoot = False AndAlso IncluirUnidadBoot = False Then
If String.Compare(s.ToLower().Substring(0, 1), _
unidadBoot.ToLower().Substring(0, 1)) = 0 Then
comprobadoBoot = True
Continue For
End If
End If
Dim retType As TipoUnidades = GetDriveType(s)
If retType = TipoUnidades.Extraible _
OrElse (incluirFijos = True AndAlso retType = TipoUnidades.Fijo) Then
extraibles.Add(s)
End If
Next
Return extraibles.ToArray
End Function
En realidad, este es casi todo el código del módulo, ya que solo falta la
definición de la estructura y la función del API para comprobar el tipo de
unidad... en fin...
Te explico las cosas que he añadido.
He añadido una variable pública (un campo que lo llaman) para indicar si
se debe incluir el disco de arranque en las unidades devueltas (IncluirUnidadBoot).
Inicialmente tiene un valor False, con idea de que no se
incluya.
La función EsBoot sirve para comprobar si la unidad del
path pasado como argumento es la unidad de arranque.
En esa función se comprueba que no se indique una cadena vacía, (ahí es
donde se usa la función Len, aunque también se podría usar
el método IsNullOrEmpty de la clase String,
pero...). La comprobación se hace comparando esa unidad con la que se ha
comprobado en el constructor del módulo (la variable UnidadBoot).
Esa variable (UnidadBoot) la he definido como de solo
lectura, y se asigna en el constructor; recuerda que las variables (o
campos) de solo lectura solo se pueden asignar en el constructor o al
definirlas.
En el constructor se asigna esa variable usando el valor devuelto por
Environment.SystemDirectory que nos dará el directorio del
sistema, por ejemplo: C:\Windows\System32).
En realidad he "complicado" un poco más de la cuenta el código del
constructor, ya que usando solo el código que hay en el Catch
(o el que hay al principio del Try), sería suficiente, ya
que, cuando se usa la variable UnidadBoot solo se comprueba
la primera letra, pero... nunca está de más hacer comprobaciones extras...
El método (función) EsExtraible sirve para comprobar si
una unidad es extraíble o no. El que esté en un Try es por
si se produce un error al intentar averiguar el tipo de unidad. Esta función
se usa desde el código del formulario.
La función UnidadesExtraibles la he cambiado para que
acepte una sobrecarga, en la que se le indica si se deben incluir las
unidades fijas, con idea de que se pueda devolver un array en el que incluya
tanto las unidades extraíbles como las fijas, ya que en el código del
formulario ahora se puede hacer una llamada a este método para que tenga en
cuenta esas unidades fijas.
En el bucle que recorre todas las unidades devueltas por el método
GetLogicalDrives de la clase Environment,
se comprueba si se debe tener en cuenta la unidad de arranque (boot) o no.
Nota:
Si te preguntas porqué hago todas esas comprobaciones de cuál es la unidad
de arranque en lugar de usar el disco C, es porque no siempre el disco C es
el disco de arranque. Aunque en Windows Vista "normalmente" siempre es el
disco C, pero en otros sistemas operativos, como el XP o anteriores, es
posible que la unidad en la que está el sistema sea otra diferente.
Sigamos viendo los cambios en el código, ahora le toca el turno al
formulario principal.
Public Sub New()
' This call is required by the Windows Form Designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Dim fic As String
With My.Application.Info
Dim appData As String = _
My.Computer.FileSystem.SpecialDirectories.MyDocuments & "\" & .ProductName
If My.Computer.FileSystem.DirectoryExists(appData) = False Then
My.Computer.FileSystem.CreateDirectory(appData)
End If
fic = appData & "\" & .ProductName & ".cfg"
End With
' Crear el objeto de configuración
cfg = New ConfigXml(fic, False)
' Para que quede claro que no se incluirá (16/Dic/07)
' la unidad de arranque
UtilDiscos.IncluirUnidadBoot = False
End Sub
Te muestro el código completo del constructor, pero en realidad lo único
que hay nuevo es la última asignación, que no es necesaria, pero como digo
en los comentarios, para que quede claro que no se permitirá usar la unidad
de arranque como disco de destino.
' Nuevo valor de configuración (16/Dic/07)
.DestinoNoUSB = cfg.GetValue("General", "DestinoNoUSB", .DestinoNoUSB)
Esta línea la agregas en el método leerCfg, justo después de asignar el
valor de UtilidadCol.
Por supuesto, debes agregar a la configuración (Settings) el valor
DestinoNoUSB con un valor predeterminado False.
En ese mismo método, casi al final (antes del End With), añade este otro
código:
' Nuevo valor de configuración (16/Dic/07)
chkDestinoNoUSB.Checked = .DestinoNoUSB
Lo que te muestro ahora es para que lo incluyas en el método guardarCfg...
espero que sepas dónde ponerlo...
Vale... como pista, el primero antes del .Save y el segundo antes del
cfg.Save:
' Nuevo valor de configuración (16/Dic/07)
.DestinoNoUSB = chkDestinoNoUSB.Checked
' Nuevo valor de configuración (16/Dic/07)
cfg.SetValue("General", "DestinoNoUSB", .DestinoNoUSB)
Como te puedes imaginar, todo esto es para tener en cuenta los valores
guardados en el fichero de configuración y para asignar el valor al
CheckBox chkDestinoNoUSB del grupo de las
configuraciones.
Vamos que esto es algo que habrá que hacer con cada nueva opción que
pongamos... ¿no? pues eso... ;-)))
Private Sub hayUnidadExtraible()
Static yaEstoy As Boolean
' No permitir la reentrada
If yaEstoy Then Exit Sub
yaEstoy = True
Dim hayExtraible As Boolean = False
' Para ver si la unidad extraíble coincide con el destino
Dim coinciden As Boolean = False
Dim comprobarLaUnidad As Boolean = False
Dim elExtraible As String = Me.cboDestino.Text
If String.IsNullOrEmpty(elExtraible) = False Then
elExtraible = elExtraible.Substring(0, 3)
comprobarLaUnidad = True
End If
Try
Dim unidades() As String
' Se pueden indicar que se usen los discos fijos (16/Dic/07)
unidades = UtilDiscos.UnidadesExtraibles(chkDestinoNoUSB.Checked)
For Each dir As String In unidades
If String.IsNullOrEmpty(dir) = False Then
hayExtraible = True
If comprobarLaUnidad = False Then
coinciden = False
Exit For
Else
If dir = elExtraible Then
coinciden = True
End If
End If
End If
Next
Catch ex As Exception
End Try
If hayExtraible = False Then
' si no hay, usar un intervalo de 1 segundo
timerComprobarExtraibles.Interval = 1000
Else
' si hay, usar un intervalo de 3 segundos
timerComprobarExtraibles.Interval = 3000
End If
statusComprobando.BackColor = Color.Red
statusComprobando.ForeColor = Color.White
statusComprobando.Visible = False
If hayExtraible Then
' Si la unidad de destino no existe
If coinciden = False Then
statusComprobando.Text = "La unidad de destino no existe"
statusComprobando.Visible = True
Else
' Si la unidad de destino no es extraíble
' indicarlo como aviso, no como error
If UtilDiscos.EsExtraible(Me.cboDestino.Text) = False Then
statusComprobando.BackColor = Color.FromKnownColor(KnownColor.Control)
statusComprobando.ForeColor = Color.Green
statusComprobando.Visible = True
statusComprobando.Text = "El destino no es extraíble"
End If
End If
Else
' Mostrar el mensaje de forma más adecuada (16/Dic/07)
If chkDestinoNoUSB.Checked = False Then
statusComprobando.Text = "NO HAY UNIDAD EXTRAIBLE"
Else
statusComprobando.Text = "No hay unidades fijas o extraíbles"
End If
statusComprobando.Visible = True
End If
' El botón de copiar habilitarlo
' solo si hay unidades válidas y coincide con el destino
btnCopiar.Enabled = coinciden
yaEstoy = False
End Sub
Este otro código lo tienes que usar tal como está, y reemplaza al que ya
había. Ya que aquí, además de tener en cuenta si se permiten unidades fijas
(no USB), pues se tienen en cuenta, además de que ahora se muestra un aviso
si el destino es una unidad fija, con idea de que el usuario sepa que no se
está copiando a una unidad extraíble.
No te lo explico paso a paso, que ya es tarde, además de que supongo que
sabrás que cambios son los que he hecho (tienen la fecha del 16) y... bueno,
no hay nada especial que resaltar... simplemente observa el código y así
sabrás qué es lo que se hace en cada caso.
Nota:
Aunque no te muestro el código, debes saber que debido a que he quitado el
control chkComprobarDiscos, tendrás que quitar el código
que lo usa en los métodos correspondientes, (no te preocupes que el editor
de Visual Basic te avisa dónde lo estás usando).
Private Sub asignarFic(ByVal cboDir As Control, ByVal e As System.Windows.Forms.DragEventArgs)
' Para asignar todos los directorios soltados
Dim dirs() As String
dirs = CType(e.Data.GetData("FileDrop", True), String())
Dim sFic As String = ""
' Si es el combo de destino (16/Dic/07)
' comprobar si se debe permitir la unidad de arranque
'
' En teoría esto no debe ocurrir,
' porque ya se comprueba en el DragEnter, pero...
'
If cboDir Is cboDestino Then
Dim destinos As New List(Of String)
For Each s As String In dirs
' Si es la unidad de arranque, no usarla
' si el valor de UtilDiscos.IncluirUnidadBoot es False
If UtilDiscos.IncluirUnidadBoot = False _
AndAlso UtilDiscos.EsBoot(s) Then
Continue For
Else
destinos.Add(s)
End If
Next
' En la unidad de destino solo permitir un directorio (16/Dic/07)
' (si no hay nada, dejar lo que hubiera)
If destinos.Count > 0 Then
sFic = destinos(0)
cboDir.Text = sFic
End If
Exit Sub
End If
' Aquí llegará si el combo no es el de destino
sFic = String.Join(";", dirs)
If String.IsNullOrEmpty(cboDir.Text) = False Then
cboDir.Text &= ";" & sFic
Else
cboDir.Text = sFic
End If
End Sub
Este método es el que se usa cuando se sueltan ficheros en un combo, así
que, hay que tener en cuenta si es el de la unidad de destino (cboDestino),
en cuyo caso, se comprueba si se debe permitir la unidad de arranque, etc.
Como digo en el comentario, en realidad no hace falta hacer ninguna
comprobación, y en teoría podrías dejar el código que había antes, ya que al
arrastrar los ficheros se comprueba si se debe permitir o no una operación
de Drag & Drop.
' Mostrar la utilidad y los parámetros (16/Dic/07)
expanderHeader.Text = expanderHeader.Tag.ToString & _
" (" & System.IO.Path.GetFileName(cboUtilidad.Text) & _
" " & cboParametros.Text & ")"
Este código lo pones en el método expanderPic_Click y
sustituye a la asignación que había antes, ya que ahora se deben mostrar los
parámetros usados con la utilidad externa (antes solo se mostraba el nombre
de la utilidad).
''' <summary>
''' Comprobar si alguna unidad extraíble tiene el fichero gsCopia.txt
''' de ser así, se usará como destino
''' Si no se encuentra ninguna, se usará la primera hallada
''' </summary>
''' <remarks></remarks>
Private Sub btnComprobarDestino_Click() Handles btnComprobarDestino.Click
Dim laUnidad As String = ""
Try
Me.Cursor = Cursors.WaitCursor
cboDestino.Text = ""
statusInfo.Text = "Comprobando unidades extraíbles..."
Me.Refresh()
Dim unidades() As String
' Si se deben incluir los discos fijos (16/Dic/07)
' (salvo el de arranque)
unidades = UtilDiscos.UnidadesExtraibles(chkDestinoNoUSB.Checked)
For Each dir As String In unidades
If String.IsNullOrEmpty(dir) = False Then
' Asignar a laUnidad, la primera unidad extraíble
' esto servirá para los casos en que ninguna tenga el fichero gsCopia.txt
' en ese caso, se usará la primera unidad extraíble.
If String.IsNullOrEmpty(laUnidad) Then
laUnidad = dir
End If
Dim fic As String = dir & If(dir.EndsWith("\"), "", "\") & "gsCopia.txt"
' si da error, ignorarlo y seguir comprobando
Try
If File.Exists(fic) Then
laUnidad = dir
Exit For
End If
Catch ex As Exception
End Try
End If
Next
Catch ex As Exception
MessageBox.Show("Error: " & ex.Message, _
"Comprobar unidades", _
MessageBoxButtons.OK, _
MessageBoxIcon.Exclamation)
Finally
Me.Cursor = Cursors.Default
cboDestino.Text = laUnidad
statusInfo.Text = statusInfo.Tag.ToString
' Si no hay unidad extraíble,
' deshabilitar el botón de copia
btnCopiar.Enabled = Not String.IsNullOrEmpty(laUnidad)
If btnCopiar.Enabled = False Then
statusInfo.Text = "¡Atención, no hay unidades extraíbles!"
End If
End Try
End Sub
En el código anterior, todo esto estaba en otro método independiente,
pero ahora lo he incluido dentro del método de evento, ya que ese otro
método no se usaba en más sitios.
En realidad, lo único que ha cambiado es la forma de llamar al método
UnidadesExtraibles de la clase UtilDiscos,
y es que ahora se le pasa si se deben tener en cuenta o no las unidades
fijas (además de las extraíbles).
Private Sub btnSeleccionarDes_Click() Handles btnSeleccionarDes.Click
Dim oFD As New System.Windows.Forms.FolderBrowserDialog
With oFD
.Description = "Seleccionar el directorio de destino"
.RootFolder = Environment.SpecialFolder.MyComputer
If .ShowDialog = System.Windows.Forms.DialogResult.OK Then
' No permitir usar el disco de arranque (16/Dic/07)
' (si así está en UtilDiscos)
If UtilDiscos.IncluirUnidadBoot = False AndAlso UtilDiscos.EsBoot(.SelectedPath) Then
MessageBox.Show("No se puede seleccionar la unidad de arranque como destino", _
"Copiar", _
MessageBoxButtons.OK, _
MessageBoxIcon.Exclamation)
cboDestino.Focus()
Else
Me.cboDestino.Text = .SelectedPath
End If
End If
End With
End Sub
Este es el nuevo código del método de evento para seleccionar el destino,
que como puedes observar, se comprueba si se debe tener en cuenta la unidad
de arranque. Aquí es uno de los sitios en el que se usa el método
EsBoot de la clase UtilDiscos, con idea de saber
si esa unidad es la de arranque o no.
' Quitado cobDestino.DragEnter, (16/Dic/07)
' ya que se comprueba por separado
Private Sub Form1_DragEnter(ByVal sender As Object, _
ByVal e As DragEventArgs) _
Handles Me.DragEnter, _
cboOrigen.DragEnter, cboUtilidad.DragEnter
' Drag & Drop, comprobar con DataFormats
If e.Data.GetDataPresent(DataFormats.FileDrop) Then
e.Effect = DragDropEffects.Copy
End If
End Sub
' Comprobar por separado en el combo de destino (16/Dic/07)
Private Sub cboDestino_DragEnter(ByVal sender As Object, _
ByVal e As DragEventArgs) _
Handles cboDestino.DragEnter
If e.Data.GetDataPresent(DataFormats.FileDrop) Then
' Para asignar todos los directorios soltados
Dim dirs() As String
dirs = CType(e.Data.GetData("FileDrop", True), String())
' Comprobar si se indica el disco de arranque
' y no se permite usarlo
If UtilDiscos.IncluirUnidadBoot = False Then
' Por si se arrastran varias y alguna no es el boot
' (no se cómo lo harán, pero... nunca se sabe, je, je)
Dim todasBoot As Boolean = True
For Each s As String In dirs
If UtilDiscos.EsBoot(s) = False Then
todasBoot = False
Exit For
End If
Next
If todasBoot Then
e.Effect = DragDropEffects.None
Exit Sub
End If
End If
e.Effect = DragDropEffects.Copy
End If
End Sub
Aunque el primer método de estos dos que te muestro (sí,
los de arriba, ¿es que no te habías enterado que normalmente estoy
explicando las cosas después de mostrar el código? si es que... en fin...
menos mal que no soy el único despistado, je, je). Como te decía, el
primer método sigue igual, solo que ahora no se usa para detectar el evento
DragEnter del control cboDestino, ya que
ese evento se comprueba en el segundo método, con idea de hacer la
comprobación (si es necesaria) de que no se pueda arrastrar nada de la
unidad de arranque, de esa forma, se podrá mostrar el icono de prohibido si
se intenta hacer eso... (ver la figura 2).
Y estos son todos los cambios... de todas formas, recuerda que en el ZIP
con el proyecto completo, está todo esto ya hecho, pero bueno, por si te
interesa ir viendo "en vivo" las cosas que he cambiado...
Nota:
Aunque estos cambios los publico en realidad el 17 de Diciembre (a la una y
pico de la madrugada), debido a que empecé a escribir todo esto a las 19
horas del día 16, pues... en todos los links aparece el día 16, que es en
realidad cuando terminé de modificar el código de la utilidad, así que...
para no ponerme a cambiarlo todo, lo dejo como 16.
Y esto es todo... por ahora...
Nos vemos.
Guillermo