lunes 3 de noviembre de 2008

Nueva versión del programa para practicar vocabulario de inglés

Después de muchos meses sin haber publicado nada, tiempo durante el cual he tenido mi primer hijo, quería compartir con todos una nueva versión del programa que escribí para poder practicar vocabulario de inglés de forma sencilla.

Seguir Leyendo...

El programa está escrito usando Visual Basic .NET, y aquellos que quieran entender cómo ha sido programado deberían leer (si no lo han hecho ya) el post en el que se explica la primera versión de este programa.

Las mejoras introducidas en esta nueva versión son dos principalmente. En primer lugar es posible seleccionar el archivo que contiene las traducciones de las palabras que queremos practicar, lo cual nos permitirá por ejemplo tener el vocabulario separado por temas. Además se ha introducido una opción de menú que nos permite añadir al archivo de vocabulario con el que estemos trabajando una nueva palabra y su traducción.

Ahora el aspecto que tiene programa al abrirlo es el siguiente.

Se ha añadido una barra de menús en la parte superior del formulario que nos permite acceder a las siguientes opciones:

  • Configuración > Seleccionar Fichero: Nos abre un cuadro de dialogo desde el que podemos elegir el fichero que contiene las traducciones de las palabras.
  • Configuración > Añadir palabra: Nos abre un formulario en el que podemos indicar una palabra en castellano y su correspondiente traducción en ingles. Dicha palabra y su traducción son añadidas al fichero de traducciones con el que estemos trabajando en ese momento (siempre y cuando no existieran ya en el mismo).
  • Acerca de: Típico formulario de información con la versión del programa, los datos del autor, y la licencia a la que está sujeta el programa (GPL en este caso).

El código que se ejecuta al hacer click sobre la opción de menú "Seleccionar Fichero" es el siguiente.

Private Sub SeleccionarFicheroToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles SeleccionarFicheroToolStripMenuItem.Click
Dim sw As StreamWriter

If OpenFileDialog.ShowDialog() = Windows.Forms.DialogResult.OK Then
rutaArchivo = OpenFileDialog.FileName
sw = New StreamWriter("configuracion.txt", False, System.Text.Encoding.Default)
sw.WriteLine(rutaArchivo)
sw.Close()
cbSentidoTraduccion.Enabled = False
txtRespuestaIntroducida.Enabled = False
btnNuevaPregunta.Enabled = False
lblAciertos.Text = "Cargando Diccionario ..."
BackgroundWorker.WorkerReportsProgress = True
BackgroundWorker.RunWorkerAsync()
End If
End Sub

Se utiliza la clase "OpenFileDialog" para mostrar el típico cuadro de dialogo que permite al usuario navegar por el sistema y seleccionar un fichero concreto. Al pulsar el botón OK en dicho cuadro de dialogo se copia la ruta completa del fichero seleccionado en la variable "rutaArchivo". La variable "rutaArchivo" es publica y por tanto puede ser accedida desde otros formularios, como por ejemplo el formulario que nos va a permitir añadir traducciones. También se escribe la ruta del fichero en un archivo de texto llamado "configuracion.txt". Como veremos más adelante esto nos va a permitir que la siguiente vez que ejecutemos el programa sigamos trabajando sobre el último archivo de vocabulario que hayamos seleccionado.

Dado que se ha seleccionado un nuevo archivo de vocabulario es necesario reconstruir el diccionario. Como ya se explicó en el post de la primera versión del programa, la función que crea el diccionario a partir de los contenidos del archivo de texto seleccionado se ejecuta en un segundo hilo de ejecución, utilizando para ello un componente de tipo "BackgroundWorker". La forma de iniciar la función que crea el diccionario es llamar al método "RunWorkerAsync" del componente "BackgroundWorker". Una barra de progreso, situada en la parte inferior del formulario, nos indicará cómo va evolucionando el proceso de carga del diccionario.

Para conseguir que la siguiente vez que el programa se inicie sigamos trabajando sobre el último archivo de vocabulario que hayamos seleccionado se ha modificado el evento de carga del formulario principal. Este es el código que se utiliza ahora.

Private Sub frmPrincipal_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim fi As FileInfo
Dim sr As StreamReader

Me.Icon = My.Resources.Resource.book
cbSentidoTraduccion.SelectedIndex = 0

fi = New FileInfo("configuracion.txt")
If fi.Exists Then
sr = New StreamReader("configuracion.txt", System.Text.Encoding.Default)
rutaArchivo = sr.ReadLine()
sr.Close()
fi = New FileInfo(rutaArchivo)
If fi.Exists Then
BackgroundWorker.WorkerReportsProgress = True
BackgroundWorker.RunWorkerAsync()
Else
MsgBox("No se ha podido encontrar el archivo para crear el diccionario", MsgBoxStyle.Critical, "Error al cargar el diccionario")
End If
Else
MsgBox("No se ha podido encontrar el archivo para crear el diccionario", MsgBoxStyle.Critical, "Error al cargar el diccionario")
End If
End Sub

En la primera versión del programa el diccionario con las traducciones se construía a partir del contenido de un fichero fijo llamado "vocabulario.dic" que tenía que estar alojado en el mismo directorio que el ejecutable. Ahora lo que se hace primero es comprobar que en el mismo directorio que el ejecutable existe un fichero llamado "configuracion.txt". Se abre dicho fichero y se lee la primera línea de texto. Dicha línea de texto contiene la ruta del archivo a partir del cuál se va a construir el diccionario, y por tanto su contenido se guarda en la variable "rutaArchivo" para después llamar al método "RunWorkerAsync" del componente "BackgroundWorker". En caso de que no se encontrase el archivo "configuracion.txt", o el archivo cuya ruta está guardada dentro del mismo, el programa se detendría indicando que se ha producido un error.

Al seleccionar la opción "Añadir palabra" del menú "Configuración" se muestra el siguiente formulario.

Este formulario contiene dos controles "EditBox" uno para introducir la palabra en castellano, y el otro para introducir la traducción correspondiente al inglés. Una vez indicada una palabra y su traducción basta con pinchar sobre el botón "Añadir Palabra" y se ejecutará el siguiente código.

Private Sub btnIntroducirPalabra_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnIntroducirPalabra.Click
Dim sw As StreamWriter
Dim traduccionesExistentes As String()
Dim escribirTraduccion As Boolean = True
Dim i As Integer

If frmPrincipal.Diccionario.CastellanoIngles.ContainsKey(txtPalabraCastellano.Text) Then
traduccionesExistentes = frmPrincipal.Diccionario.CastellanoIngles(txtPalabraCastellano.Text).ToString().ToUpper().Split(","c)
For i = 0 To traduccionesExistentes.GetUpperBound(0)
If txtPalabraIngles.Text.ToUpper() = traduccionesExistentes(i) Then
escribirTraduccion = False
Exit For
End If
Next
If escribirTraduccion = True Then
sw = New StreamWriter(frmPrincipal.rutaArchivo, True, System.Text.Encoding.Default)
sw.WriteLine(txtPalabraCastellano.Text & " = " & txtPalabraIngles.Text)
sw.Close()
End If
Else
sw = New StreamWriter(frmPrincipal.rutaArchivo, True, System.Text.Encoding.Default)
sw.WriteLine(txtPalabraCastellano.Text & " = " & txtPalabraIngles.Text)
sw.Close()
End If
txtPalabraCastellano.Text = ""
txtPalabraIngles.Text = ""
txtPalabraCastellano.Focus()
End Sub

Lo primero que se hace es comprobar si la palabra en castellano que hemos escrito existe ya en el diccionario de vocabulario o no. Si no existe se obtiene un stream a través del cual poder escribir en el fichero de vocabulario con el que estamos trabajando en ese momento, y se añade una línea indicando la palabra y su traducción. Como ya hemos visto antes, la ruta del fichero la extraemos de la variable "rutaArchivo". 

Si la palabra en castellano ya existe en el diccionario se extraen todas las posibles traducciones que tiene asociadas y se comparan con la traducción indicada por el usuario. Si la traducción indicada por el usuario ya está en el diccionario no es necesario escribir nada en el fichero, y si no es ninguna de las que tiene asociadas la palabra en castellano se escribe una línea en el fichero indicando la nueva traducción.

Finalmente, la opción de menú "Acerca de.." muestra el siguiente formulario informativo.

Lo único que se puede comentar relacionado con este formulario es el código que tienen asociado los controles de tipo "LinkLabel", o lo que es lo mismo, los enlaces que aparecen en azul. Al pinchar sobre el primero se nos abrirá el programa que tengamos configurado por defecto para el correo y nos permitiría enviar un mail al autor del programa, mientras que el segundo abrirá el navegador llevándonos directamente a este mismo Blog.

Private Sub LinkLabel1_LinkClicked(ByVal sender As System.Object, ByVal e As System.Windows.Forms.LinkLabelLinkClickedEventArgs) Handles LinkLabel1.LinkClicked
Process.Start("mailto:sergios.weblog@gmail.com")
End Sub

Private Sub LinkLabel2_LinkClicked(ByVal sender As System.Object, ByVal e As System.Windows.Forms.LinkLabelLinkClickedEventArgs) Handles LinkLabel2.LinkClicked
Process.Start("http://masprogramacionymenosprozac.blogspot.com")
End Sub

A continuación tenéis los enlaces para descargar tanto el proyecto completo como el ejecutable de la aplicación.

Proyecto completo para Visual Studio 2005: Descargar.

Ejecutable de la aplicación: Descargar.

sábado 19 de enero de 2008

Configuraciones sencillas en Ubuntu (I)

En este nuevo grupo de posts he querido recoger algunos ajustes que se pueden hacer en la configuración de Ubuntu 7.10 (Gutsy Gibbon) y que yo considero pueden ser de utilidad. Posiblemente otras personas consideren que algunas de estas configuraciones no son correctas, por ejemplo por temas de seguridad. De todas formas yo las presento por si a alguno le interesan, y como siempre, si alguien conoce una forma mejor o más sencilla de hacer las cosas le animo a dejar un comentario explicándolo.

Seguir Leyendo...
  1. Autoarranque de CD
  2. Alias de comandos comunes

Autoarranque de CD

La capacidad de que al insertar un CD o DVD en la unidad lectora se ejecute automáticamente un determinado programa que tengamos en el CD, o se abra el navegador para mostrarnos un índice de los contenidos del CD en HTML, es algo a lo que los usuarios que habitualmente utilizan Windows están muy acostumbrados. Ubuntu Gutsy Gibbon incluye también esta funcionalidad, aunque por defecto está deshabilitada. Para habilitarla, desde el entorno gráfico, solo tenemos que ir a "Sistema > Preferencias > Unidades y soportes extraíbles" y en la ventana que se nos abre activar la opción "Autoejecutar programas en los soportes y unidades nuevos". 

Una vez activada, al insertar un CD o DVD, el sistema comprueba si en la raíz del CD tenemos un archivo ejecutable con alguno de los siguientes nombres: ".autorun", "autorun", o "autorun.sh". En caso de encontrarlo lo ejecuta automáticamente (en realidad, antes de ejecutarlo nos pide confirmación).

Sin embargo, la primera vez que yo seguí estos pasos me encontré con que la cosa seguía sin funcionar. La razón era la siguiente. Si se examina el archivo de configuración "/etc/fstab" podemos encontrarnos con que las unidades de CD se estén montando con la opción "noexec". Esto impide (por cuestiones de seguridad) que se pueda ejecutar cualquier archivo que tengamos en el CD. Si queremos poder ejecutar archivos que estén en el CD, y por extensión que nos funcione el autoarranque, tendremos que indicar que las unidades de CD o DVD se monten con la opción "exec" en vez de "noexec". Para ello basta modificar el archivo "/etc/fstab" de forma que nos quede algo así:

# /etc/fstab: static file system information.
#
#"file" "mount" "type" "options" "dump" "pass"
.
.
/dev/hdb /media/cdrom0 udf,iso9660 user,noauto,exec 0 0
/dev/hdd /media/cdrom1 udf,iso9660 user,noauto,exec 0 0
/dev/fd0 /media/floppy0 auto rw,user,noauto 0 0

NOTA: En Ubuntu 8.04 (Hardy Heron) ha cambiado la ventana de "Unidades y soportes extraíbles" y ya no aparece la opción comentada. El autoarranque de CD ya viene activado por defecto y las unidades de CD y DVD se montan con la opción "exec".

Alias de comandos comunes

Cuando se trabaja habitualmente desde la línea de comandos resulta muy útil definir alias de los comandos que más se usan para de esa forma ahorrar tiempo al teclear. Definir un alias es tan sencillo como escribir en la línea de comandos:

 alias ll='ls -lh --color=auto'

A partir de ese momento solo con escribir "ll" se ejecutaría el comando "ls -lh --color=auto". El problema es que si reiniciamos la máquina el alias que hemos definido ya no existe. La forma correcta de trabajar es, por tanto, incluir la definición de los alias dentro de alguno de los scripts de configuración que se ejecutan cada vez que un usuario arranca el interprete de comandos bash. Si examinamos el archivo ".bashrc" que se encuentra en el directorio HOME del usuario podemos encontrar una sección como la siguiente:

# Alias definitions.
# You may want to put all your additions into a separate file like
# ~/.bash_aliases, instead of adding them here directly.
# See /usr/share/doc/bash-doc/examples in the bash-doc package.

if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi

Esta sección del script comprueba si existe un fichero llamado ".bash_aliases" dentro del directorio HOME del usuario, y en tal caso ejecuta los comandos que éste contiene. Los comentarios nos sugieren que podemos utilizar el fichero ".bash_aliases" para colocar en él las definiciones de los alias que queramos usar. Tenemos entonces, que una forma limpia de que cada usuario pueda definir sus propios alias es creando un archivo ".bash_aliases" en su directorio HOME que contenga algo como lo siguiente:

alias cp='cp -i'
alias mv='mv -i'
alias rm='rm -i'
alias cd..='cd ..'
alias c='clear'
alias s='sudo'
alias ls='ls --color=auto'
alias ll='ls -lh --color=auto'
alias la='ls -Alh --color=auto'
alias lx='ls -Xlh --color=auto'
alias lk='ls -Slh --color=auto'
alias lm='ls -Alh | less'

domingo 25 de noviembre de 2007

Programa en .NET para practicar vocabulario de inglés

En esta ocasión estaba tratando de repasar algo de vocabulario de inglés y se me ocurrió hacer un pequeño programa que me ayudase. El programa va pidiendo al usuario que traduzca una serie de palabras o expresiones, y comprueba si las respuestas introducidas son correctas o no. En muchos casos una palabra puede tener varias traducciones, o existir varias palabras que tengan un significado similar. Por esta razón, el programa además de indicar si la respuesta introducida por el usuario es correcta, nos sugiere otras posibles respuestas. Se puede escoger que el sentido de la traducción sea de castellano a inglés o de inglés a castellano.

Seguir Leyendo...

Tanto las preguntas formuladas como sus posibles respuestas se extraen aleatoriamente de una especie de diccionario que se carga en memoria durante el arranque del programa. El diccionario se genera cada vez que se arranca, a partir de la información que hay en un fichero de texto llamado "vocabulario.dic", y que debe estar ubicado en el mismo directorio que el ejecutable. En este fichero cada línea es una igualdad del tipo "casa = house". Si en otra línea del fichero tenemos "casa = home", al crearse el diccionario el programa sabe que si nos pide que traduzcamos la palabra "casa" habrá dos posibles respuestas "house" y "home", y también que tanto si nos pide traducir la palabra "house" como si nos pide traducir la palabra "home" en ambos casos la respuesta correcta será casa.

El programa lo he creado en Visual Basic .NET utilizando el entorno Visual Studio 2005. La razón de esta elección es que considero que éste lenguaje permite crear interfaces gráficas de usuario de una forma bastante rápida y sencilla, y además contiene multitud de funciones y clases orientadas al manejo de cadenas de texto. Para esta aplicación creo que estas características del lenguaje son más importantes que buscar un rendimiento óptimo o un acceso de bajo nivel al sistema.

Siempre que hago un programa, aunque sea sencillo como éste, me gusta aprender algo nuevo. En este caso, los puntos del desarrollo que me parecen más interesantes son los siguientes:

  • La clase "DiccionarioIngles" está formada por dos objetos del tipo "SortedList" (uno para el sentido castellano-inglés y otro para el sentido inglés-castellano). Los objetos "SortedList" son colecciones de pares "clave - valor" que pueden ser accedidos tanto por la clave como por el índice que ocupan.
  • Dependiendo de lo grande que sea el fichero "vocabulario.dic" la carga del diccionario puede ser un proceso que lleve varios segundos. Para evitar bloquear la interfaz gráfica, este proceso se lleva a cabo en un segundo hilo de ejecución utilizando para ello un componente de tipo "BackgroundWorker". Además, una barra de progreso indica como va avanzando el proceso.
  • Se ha incluido en la aplicación un recurso de tipo icono. Este icono se utiliza después como el icono que aparece en la esquina superior izquierda del formulario principal, y como el icono para el archivo ejecutable cuando se visualiza en el explorador.

La siguiente imagen muestra el diseño de la interfaz gráfica de usuario. 

Básicamente consta de los siguientes elementos:

  • Un "ComboBox" que permite seleccionar si las traducciones serán de castellano a ingles o de ingles a castellano.
  • Un botón que cada vez que es activado formula una nueva pregunta.
  • Un "label" a través del cual se le indica al usuario la palabra o expresión a traducir.
  • Un "EditBox" donde el usuario puede escribir la respuesta.
  • Otros dos "labels" uno para indicar si la respuesta introducida ha sido correcta o no y otro en que se indican todas las posibles respuestas.
  • Y finalmente un control "StatusStrip" (la barra de estado que aparece en la parte inferior de un formulario) que contiene dos elementos. Una barra de progreso para visualizar como avanza la carga en memoria del diccionario cuando se arranca la aplicación, y un "label" que informa al usuario del número de aciertos y el número total de preguntas realizadas.

Para el diccionario que maneja el programa internamente y que asocia las palabras con sus traducciones posibles se ha creado una clase llamada "DiccionarioIngles".

Public Class DiccionarioIngles

#Region "VARIABLES"
Public CastellanoIngles As SortedList
Public InglesCastellano As SortedList
#End Region

#Region "METODOS"
Public Sub New()
CastellanoIngles = New SortedList()
InglesCastellano = New SortedList()
End Sub
#End Region

End Class

Esta clase contiene dos miembros públicos del tipo "SortedList". Un objeto "SortedList" es una colección ordenada de pares "clave - valor" que pueden ser accedidos tanto por clave como por índice. Si pensamos en que las claves sean las palabras o expresiones a traducir y sus valores asociados sean las posibles traducciones se observa claramente la similitud con un diccionario. También es fácil entender que necesitamos dos colecciones "SortedList", una para asociar palabras en castellano con sus traducciones en inglés y otra para asociar palabras en inglés con sus traducciones en castellano. El que los elementos de una colección de tipo "SortedList" sean accesibles por índice, resulta de especial interés a la hora de extraer aleatoriamente uno de los pares "clave - valor" como veremos más adelante.

Como ya se ha dicho, el diccionario se genera cada vez que arranca el programa a partir de la información contenida en el fichero "vocabulario.dic". Este proceso se ejecuta en otro hilo que corre en segundo plano. La forma más sencilla de conseguir esto es añadir al formulario principal un componente de tipo "BackgroundWorker" y asociarle el proceso de construcción del diccionario. Empezaremos por ver cómo se lanza el hilo que se ejecuta en segundo plano desde el evento "Load" del formulario principal.

Private Sub frmPrincipal_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim fi As FileInfo

Me.Icon = My.Resources.Resource.book
cbSentidoTraduccion.SelectedIndex = 0

fi = New FileInfo("vocabulario.dic")
If fi.Exists Then
BackgroundWorker.WorkerReportsProgress = True
BackgroundWorker.RunWorkerAsync(fi.Length() + 2)
Else
MsgBox("No se ha podido encontrar el archivo ""vocabulario.dic""", MsgBoxStyle.Critical, "Error al cargar el diccionario")
End
End If
End Sub

Utilizando la clase "FileInfo" comprobamos en primer lugar si el fichero "vocabulario.dic" existe. Si no existiese simplemente se mostraría un mensaje de error para avisar al usuario y se finalizaría la aplicación. Sin embargo, lo normal es que el fichero exista y que el programa llame al método "RunWorkerAsync" del objeto "BackgroundWorker". Esta llamada da lugar a que se cree un segundo hilo que ejecutará el código contenido en el evento "DoWork" del objeto "BackgroundWorker". Además, al llamar al método "RunWorkerAsync" estamos pasándole un parámetro que después estará accesible en el evento "DoWork". Este parámetro es el tamaño del fichero "vocabulario.dic" y lo necesita el segundo hilo para poder ir reportando periódicamente el tanto por ciento del fichero que se ha leído hasta ese momento. También podemos ver que para que el segundo hilo pueda ir reportando cómo avanza en la lectura del fichero es necesario poner la propiedad "WorkerReportsProgress" a "true".

Otro punto interesante es que este evento "Load" contiene un ejemplo de cómo indicar que el icono del formulario va a ser un icono que hemos metido dentro del ejecutable en forma de recurso. En este caso, el icono se llama "book.ico" y podemos referirnos a él utilizando "My.Resources.Resource.Book".

A continuación se muestra el código del evento "DoWork" del "BackgroundWorker" que es donde realmente se construye el diccionario.

Private Sub BackgroundWorker_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker.DoWork
Dim Diccionario As New DiccionarioIngles()
Dim sr As StreamReader
Dim linea As String
Dim lineaSeparada As String()
Dim tamanoArchivo As Long
Dim bytesLeidos As Long

tamanoArchivo = CLng(e.Argument)
sr = New StreamReader("vocabulario.dic", System.Text.Encoding.Default)
linea = sr.ReadLine()
Do While linea IsNot Nothing
'System.Threading.Thread.Sleep(1) 'Solo para que se vea la carga del diccionario
lineaSeparada = linea.Split("="c)
lineaSeparada(0) = lineaSeparada(0).Trim()
lineaSeparada(1) = lineaSeparada(1).Trim()
If Diccionario.CastellanoIngles.ContainsKey(lineaSeparada(0)) Then
Diccionario.CastellanoIngles(lineaSeparada(0)) &= ("," & lineaSeparada(1))
Else
Diccionario.CastellanoIngles.Add(lineaSeparada(0), lineaSeparada(1))
End If
If Diccionario.InglesCastellano.ContainsKey(lineaSeparada(1)) Then
Diccionario.InglesCastellano(lineaSeparada(1)) &= ("," & lineaSeparada(0))
Else
Diccionario.InglesCastellano.Add(lineaSeparada(1), lineaSeparada(0))
End If
bytesLeidos += linea.Length + 2
BackgroundWorker.ReportProgress((bytesLeidos / tamanoArchivo) * 100)
linea = sr.ReadLine()
Loop
e.Result = Diccionario
End Sub

El tamaño del fichero "vocabulario.dic", pasado como parámetro a la función "RunWorkerAsync", está disponible dentro del evento "DoWork" a través de "e.Argument".

La tarea que se lleva a cabo dentro del evento "DoWork" consiste básicamente en ir leyendo el fichero "vocabulario.dic" línea a línea. Como ya se ha dicho, cada línea contiene una igualdad del tipo "casa = house". Utilizando el método "Split" separamos la parte que queda a la izquierda del igual (la palabra en castellano), de la que queda a la derecha del igual (su traducción en inglés). Primero se toma la palabra en castellano y se comprueba si ya esta metida dentro de la sección castellano-inglés del diccionario. Si no lo está se añade el par clave-valor correspondiente con la palabra y su traducción. Si la palabra ya existe se añade la traducción a las que ya se hayan metido previamente. De forma análoga, se comprueba si la palabra en inglés ya existe dentro de la sección inglés-castellano del diccionario, y en función de esto se crea una nueva entrada en el diccionario o simplemente se añade la palabra en castellano a las otras posibles traducciones que ya se hayan metido antes.

Una vez se ha leído totalmente el fichero "vocabulario.dic" el evento "DoWork" termina, y con él desaparece el segundo hilo de ejecución que se había creado. Se lanza entonces el evento "RunWorkerCompleted", pero este evento se ejecuta ya en el contexto del hilo principal de la aplicación. La forma que tiene el hilo que se ejecuta en segundo plano de pasar el resultado de su trabajo al hilo principal es a través de la variable "e.Result". Justo antes de finalizar el evento "DoWork" hacemos que "e.Result" contenga el diccionario construido a partir del fichero "vocabulario.dic"

Por otra parte tenemos que cada vez que se lee una línea del fichero se actualiza un contador de bytes leídos, y a partir de él y del tamaño total del fichero podemos saber el tanto por ciento del fichero que se ha leído hasta ese momento. Se llama entonces al método "ReportProgress" del "BackgroundWorker". Esta llamada provoca la ejecución del evento "ProgressChanged" que se ejecuta en el contexto del hilo principal y que por tanto tiene acceso a los elementos de la interfaz gráfica de usuario.

A continuación tenemos el código de los eventos "ProgressChanged" y "RunWorkerCompleted".

Private Sub BackgroundWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker.ProgressChanged
BarraProgreso.Value = CInt(e.ProgressPercentage)
End Sub

Private Sub BackgroundWorker_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker.RunWorkerCompleted
Diccionario = CType(e.Result, DiccionarioIngles)
TablaActual = Diccionario.CastellanoIngles

cbSentidoTraduccion.Enabled = True
txtRespuestaIntroducida.Enabled = True
lblAciertos.Text = "Aciertos: 0/0"

Randomize()
IndicePregunta = CInt(TablaActual.Count * Rnd())
lblPregunta.Text = TablaActual.GetKey(IndicePregunta)
txtRespuestaIntroducida.Focus()
End Sub

El evento "ProgressChanged" simplemente actualiza una barra de progreso para reflejar visualmente el tanto por ciento del fichero "vocabulario.dic" leído hasta ese momento.

En el evento "RunWorkerCompleted" se coge desde "e.Result" el diccionario que se ha construido previamente, y se mete en una variable accesible desde cualquier otro evento. También se inicializan algunos de los elementos de la intefaz gráfica, y finalmente se obtiene un índice al azar que permite acceder a la sección castellano-inglés del diccionario y mostrar al usuario una palabra en castellano para que éste la traduzca.

Cuando el usuario escribe la respuesta en el "EditBox" correspondiente y pulsa la tecla ENTER se ejecuta el código que comprueba si la traducción que ha escrito el usuario coincide con alguna de las traducciones que tiene asociadas la palabra en el diccionario. Si es así se muestra la palabra "CORRECTO" en color azul, en caso contrario se muestra la palabra "INCORRECTO" en rojo. En cualquiera de los casos se le indican al usuario todas las posibles traducciones, y se actualiza el "label" que muestra cuantos aciertos llevamos de entre todas las respuestas que se hayan escrito.

Private Sub txtRespuestaIntroducida_KeyPress(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtRespuestaIntroducida.KeyPress
Dim respuestasPosibles() As String
Dim respuestaAcertada As Boolean
Dim i As Integer

If e.KeyChar = Chr(13) Then
respuestasPosibles = TablaActual.GetByIndex(IndicePregunta).ToString().ToUpper().Split(","c)

For i = 0 To respuestasPosibles.GetUpperBound(0)
If txtRespuestaIntroducida.Text.ToUpper() = respuestasPosibles(i) Then
respuestaAcertada = True
Exit For
End If
Next

If respuestaAcertada = True Then
lblResultado.ForeColor = Color.Blue
lblResultado.Text = "CORRECTO"
lblResultado.Visible = True
RespuestasAcertadas = RespuestasAcertadas + 1
Else
lblResultado.ForeColor = Color.Red
lblResultado.Text = "INCORRECTO"
lblResultado.Visible = True
End If

lblRespuestasPosibles.Text = "Respuestas posibles: " & TablaActual.GetByIndex(IndicePregunta).ToString()
PreguntasRealizadas = PreguntasRealizadas + 1
lblAciertos.Text = "Aciertos: " & RespuestasAcertadas & "/" & PreguntasRealizadas
btnNuevaPregunta.Focus()
End If
End Sub

Cuando se actúa sobre el "ComboBox" que permite al usuario seleccionar el sentido de la traducción, básicamente lo que se hace a nivel interno es seleccionar si la variable "TablaActual" de tipo "SortedList" apunta a la sección castellano-inglés del diccionario o a la sección inglés-castellano. Esta variable "TablaActual" es la que se utiliza en el resto de eventos para acceder a la tabla de palabras y traducciones asociadas independientemente del sentido en que sea la traducción.

Private Sub cbSentidoTraduccion_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cbSentidoTraduccion.SelectedIndexChanged
If Diccionario IsNot Nothing Then
If cbSentidoTraduccion.SelectedIndex = 0 Then
TablaActual = Diccionario.CastellanoIngles
Else
TablaActual = Diccionario.InglesCastellano
End If
lblResultado.Visible = False
txtRespuestaIntroducida.Text = ""
lblRespuestasPosibles.Text = "Respuestas posibles: "
IndicePregunta = CInt(TablaActual.Count * Rnd())
lblPregunta.Text = TablaActual.GetKey(IndicePregunta)
txtRespuestaIntroducida.Focus()
End If
End Sub

Finalmente, tendríamos el código del evento que se ejecuta al pulsar el botón de nueva pregunta y que se muestra a continuación. No creo que sea necesario explicar este código.

Private Sub btnNuevaPregunta_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnNuevaPregunta.Click
lblResultado.Visible = False
txtRespuestaIntroducida.Text = ""
lblRespuestasPosibles.Text = "Respuestas posibles: "
IndicePregunta = CInt(TablaActual.Count * Rnd())
lblPregunta.Text = TablaActual.GetKey(IndicePregunta)
txtRespuestaIntroducida.Focus()
End Sub

La verdad es que no sé si alguien habrá sido capaz de leerse todo este tocho de explicación pero bueno, si alguien ha llegado hasta aquí solo me queda decir que como siempre cualquiera que tenga una duda o sugerencia solo tiene que animarse a dejar un comentario. Al fin y al cabo una de las mejores oportunidades que brinda internet es poder aprender unos de otros.

A continuación tenéis los enlaces para descargar tanto el proyecto completo como el ejecutable de la aplicación.

Proyecto completo para Visual Studio 2005: Descargar.

Ejecutable de la aplicación: Descargar.

domingo 14 de octubre de 2007

Script de Linux para administrar colecciones de comics

Hola a todos de nuevo.

Últimamente es bastante común encontrar en la red comics o revistas en formato CBR o CBZ (además de en otros formatos más conocidos como el PDF). Los archivos CBR y CBZ no son más que grupos de imágenes comprimidos en formato RAR o ZIP correspondientemente, y a los que se les ha cambiado la extensión. Estos archivos pueden ser manejados directamente por programas como el CDisplay en Windows o el Comix en Linux.

Seguir Leyendo...

Cuando se ha conseguido reunir una colección de comics formada por multitud de archivos de diferentes formatos resulta interesante poder hacer una copia a CD o DVD. Y ya puestos, ¿Por qué no incluir en dicha copia un índice en formato HTML de nuestra colección que incluya miniaturas de las portadas de todos nuestros comics? ¿Y por qué no hacer que el CD se arranque automáticamente al meterlo en el lector y nos muestre el índice en nuestro navegador?

Construir un índice de este tipo puede resultar una tarea realmente tediosa. Por ejemplo, solo para incluir la portada de un archivo CBR tendríamos que descomprimirlo y extraer su contenido, coger la primera imagen y redimensionarla para obtener una miniatura con el tamaño adecuado, y finalmente incluir en el índice el código HTML correspondiente para visualizar la miniatura y enlazarla con el archivo CBR.

Para automatizar toda la tarea de generación del índice y grabación del CD con autoarranque he escrito un script de bash con las siguientes características:

  • Recorre un árbol de directorios creando un índice en formato HTML por cada directorio.
  • Cada índice (correspondiente a un directorio) contiene links que enlazan con los índices HTML de sus subdirectorios y links que permiten acceder a los archivos contenidos en el propio directorio.
  • Para archivos CBR, CBZ y PDF junto al link se incluye una miniatura de la portada del cómic.
  • Para los directorios que contienen imágenes, junto a cada link asociado a una imagen aparece una miniatura de la misma.
  • Una vez generados los índices ofrece al usuario la opción de copiar directamente en un CD o DVD la colección de comics junto con los índices.
  • En el CD o DVD se copian también los archivos necesarios para que al insertarlo en el lector se abra automáticamente el índice HTML principal en el navegador (autorun.inf y autorun.sh).

La siguiente imagen muestra una captura de pantalla en la que se puede ver cómo quedan los índices HTML generados. 

El código comentado del script es el siguiente:

#!/bin/bash
#
# DESCRIPCIÓN
#
# Este script está diseñado para generar de forma automática índices HTML de un conjunto de directorios que contenga una
# colección de comics o revistas. Los formatos soportados son CBR, CBZ y PDF, así como los formatos de imagen más comunes.
# Una vez generados los índices el script permite crear directamente un CD o DVD con autoarranque que contenga nuestra
# colección de comics y los índices HTML.
#
# NOTA: Este script require tener instalado el paquete ImageMagick.
#
# LICENCIA
#
# Puede usar este script bajo licencia GPL (v2 o superior).
#
# AUTOR
#
# Sergio Salas 2007
# Web: http://masprogramacionymenosprozac.blogspot.com
# Mail: sergios.weblog@gmail.com

# Función que recibe como parámetro un texto y devuelve en la variable "textHTML" dicho texto habiendo sustituido
# ciertos caracteres especiales (vocales acentuadas, letra eñe...) por las secuencias correspondientes que representan
# dichos caracteres en el formato HTML.
Text2HTML()
{
textHTML=`echo $1 | sed -e 's/á/\á/g' -e 's/Á/\Á/g' -e 's/é/\é/g' -e 's/É/\É/g' \
-e 's/í/\í/g' -e 's/Í/\Í/g' -e 's/ó/\ó/g' -e 's/Ó/\Ó/g' -e 's/ú/\ú/g' -e 's/Ú/\Ú/g' \
-e 's/ñ/\ñ/g' -e 's/Ñ/\Ñ/g' -e 's/º/\º/g' -e 's/ª/\ª/g'`
}

# Se considera como separador de campos solo el salto de línea. Si no hacemos esto el script falla con los nombres de
# archivos o directorios que contienen espacios en blanco.
IFS=$'\n'

DIR_PROJECT=/tmp/comics-index-pages # Directorio que contendrá los índices HTML
DIR_EXTRACT=/tmp/extract # Directorio en que se extraen temporalmente los contenidos de los archivos CBR y CBZ
DEV_RECORDER=/dev/hdb # Nombre de dispositivo que corresponde al grabador de CD/DVD

clear
echo ""
echo "================================================================"
echo " Generador de índices HTML para colecciones de comics v1.0 "
echo "================================================================"
echo ""

# Se pide la ruta del directorio que contiene los comics y el título que queremos que aparezca en los índices HTML
read -p "Directorio raíz que contiene la colección: " rootdir
read -p "Título utilizado en los índices: " title

# Se comprueba si el directorio que va a contener los índices ya existe. Si es así posiblemente se deba a una ejecución
# previa del script. Para evitar una posible perdida de datos por error se le da la oportunidad al usuario de salir
# del script o bien borrar el directorio que ya existe y continuar.
if [ -d $DIR_PROJECT ]; then
echo ""
echo "El directorio \"$DIR_PROJECT\" ya existe, posiblemente debido a una ejecución anterior del script."
echo "[1] - Borrar el directorio y continuar."
echo "[2] - Salir."
read -p "Escriba la opción: " option
echo ""
case $option in
1)
rm -r $DIR_PROJECT
rm -r $DIR_EXTRACT
;;
*)
exit 1
;;
esac
fi

# Se crean los directorios que el script utiliza durante su ejecución.
mkdir -p $DIR_PROJECT
mkdir -p $DIR_EXTRACT

# Nos colocamos en el directorio raiz que contiene los comics
cd "$rootdir"

# Se buscan recursivamente los nombres de todos los subdirectorios y por cada uno se creará un fichero HTML.
for dirName in `find . -name "*" -type d | cut --delimiter=/ --fields=2-`
do
# Para el directorio raiz el nombre del fichero será "index.html". Para los subdirectorios el nombre del fichero
# será igual a la ruta del subdirectorio sustituyendo los "/" por "-" (Ej: si hay un subdirectorio que se llame
# "colección2/temporada1" el fichero HTML asociado se llamará "colección2-temporada1.html").
if [ $dirName = "." ]; then
dirNameMod=index
else
dirNameMod=`echo $dirName | tr / -`
fi
# Se crea el subdirectorio que contendrá las imagenes para esa página HTML
mkdir -p $DIR_PROJECT/$dirNameMod-images

# Título del índice HTML
echo "<html>" > $DIR_PROJECT/$dirNameMod.html
Text2HTML $title
echo "<head><title>$textHTML</title></head>" >> $DIR_PROJECT/$dirNameMod.html

# Los índices HTML contienen una tabla con los links tanto a los subdirectorios como a los archivos contenidos en el directorio
echo "<body>" >> $DIR_PROJECT/$dirNameMod.html
echo "<table width="100%" border="1" cellpadding="2" cellspacing="2">" >> $DIR_PROJECT/$dirNameMod.html

# Primero se meten los links que llevan a los índices HTML de los subdirectorios.
for subdirName in `find "$dirName" -maxdepth 1 -mindepth 1 -name "*" -type d | cut --delimiter=/ --fields=2- | sort`
do
Text2HTML $subdirName
if [ $dirNameMod = "index" ]; then
echo "<tr><td colspan="2"><a href=\"$subdirName.html\"><b>- $textHTML</b></a></td></tr>" >> $DIR_PROJECT/$dirNameMod.html
else
echo "<tr><td colspan="2"><a href=\"$dirNameMod-$subdirName.html\"><b>- $textHTML</b></a></td></tr>" >> $DIR_PROJECT/$dirNameMod.html
fi
done

# Después se meten los links que enlazan con los archivos contenidos en el directorio.
for fileName in `find "$dirName" -maxdepth 1 -name "*" -type f -printf '%f\n' | sort`
do
echo "Procesando el archivo: $dirName/$fileName"
# Se saca la extensión del archivo y en función del tipo de archivo haremos diferentes cosas
extension=`echo $fileName | cut --delimiter=. --fields=2 | tr [:lower:] [:upper:]`
fileNameMod=`echo $fileName | cut --delimiter=. --fields=1`
case $extension in
CBR)
# Para el caso de un archivo CBR se extrae temporalmente el contenido en el directorio DIR_EXTRACT. Se coge
# el primer fichero ,normalmente será la portada del comics, y se obtiene una miniatura. Se añade al índice
# el código HTML necesario para incluir la miniatura y que además esta sirva de link al archivo.
unrar e -inul "$dirName/$fileName" "$DIR_EXTRACT"
firstFile=`ls "$DIR_EXTRACT" | head -n 1`
convert -resize x250 "$DIR_EXTRACT/$firstFile" "$DIR_PROJECT/$dirNameMod-images/$fileNameMod-$firstFile"
rm -f "$DIR_EXTRACT"/*
echo "<tr><td width="10%"><a href=\"../$dirName/$fileName\"><img src=\"$dirNameMod-images/$fileNameMod-$firstFile\"></a></td>" >> $DIR_PROJECT/$dirNameMod.html
;;
CBZ)
# Los archivos CBZ se tratan de forma análoga a los archivos CBR.
unzip -jq "$dirName/$fileName" -d "$DIR_EXTRACT"
firstFile=`ls "$DIR_EXTRACT" | head -n 1`
convert -resize x250 "$DIR_EXTRACT/$firstFile" "$DIR_PROJECT/$dirNameMod-images/$fileNameMod-$firstFile"
rm -f "$DIR_EXTRACT"/*
echo "<tr><td width="10%"><a href=\"../$dirName/$fileName\"><img src=\"$dirNameMod-images/$fileNameMod-$firstFile\"></a></td>" >> $DIR_PROJECT/$dirNameMod.html
;;
PDF)
# Para el caso de un archivo PDF se obtiene directamente una miniatura de la primera página del PDF y después
# se añade al índice el código HTML necesario para incluir la miniatura y que esta se comporte como un enlace
# hacia el archivo PDF.
convert "$dirName/$fileName"[0] -resize x250 "$DIR_PROJECT/$dirNameMod-images/$fileNameMod.jpg"
echo "<tr><td width="10%"><a href=\"../$dirName/$fileName\"><img src=\"$dirNameMod-images/$fileNameMod.jpg\"></a></td>" >> $DIR_PROJECT/$dirNameMod.html
;;
BMP | GIF | JPG | JPEG)
# Para los archivos de imagen simplemente se obtiene una miniatura de la imagen que se incluye en el índice HTML
# como un enlace hacia el archivo con la imagen original.
convert "$dirName/$fileName" -resize x250 "$DIR_PROJECT/$dirNameMod-images/$fileName"
echo "<tr><td width="10%"><a href=\"../$dirName/$fileName\"><img src=\"$dirNameMod-images/$fileName\"></a></td>" >> $DIR_PROJECT/$dirNameMod.html
;;
*)
echo "<tr><td width="10%"></td>" >> $DIR_PROJECT/$dirNameMod.html
;;
esac
Text2HTML $fileName
echo "<td><a href=\"../$dirName/$fileName\"><b>- $textHTML</b></a></td></tr>" >> $DIR_PROJECT/$dirNameMod.html
done

echo "</table>" >> $DIR_PROJECT/$dirNameMod.html
echo "</body>" >> $DIR_PROJECT/$dirNameMod.html
echo "</html>" >> $DIR_PROJECT/$dirNameMod.html
done

# Una vez creados todos los índices se le dan al usuario distintas opciones
echo ""
echo "Los índices HTML han sido generados. ¿Qué desea hacer ahora?"
echo "[1] - Mover los índices al directorio raiz que contiene los comics."
echo "[2] - Grabar en un CD o DVD."
echo "[3] - Salir."
read -p "Escriba la opción: " option
echo ""

case $option in
1)
# Se mueven los índices HTML generados al mismo directorio que contiene los comics (y que es el directorio en el que
# estamos en ese momento).
mv $DIR_PROJECT .
;;
2)
# Para que al introducir el CD o DVD en el reproductor se abra automáticamente el índice general hay que incluir
# los archivo "autorun.inf" (utilizado en Windows) y "autorun" (utilizado en Linux).
echo "Start ./index-pages/index.html" > $DIR_EXTRACT/autorun.bat
echo "[AUTORUN]" > $DIR_EXTRACT/autorun.inf
echo "Label = Comics" >> $DIR_EXTRACT/autorun.inf
echo "Open = autorun.bat" >> $DIR_EXTRACT/autorun.inf
echo "#!/bin/sh" > $DIR_EXTRACT/autorun.sh
echo "firefox ./index-pages/index.html" >> $DIR_EXTRACT/autorun.sh # Se puede sustituir firefox por otro navegador
chmod 755 $DIR_EXTRACT/autorun.sh

# Se graban en un CD o un DVD los comics junto con los índices HTML y los archivos necesarios para el autoarranque
mkisofs -RJ -graft-points -o /tmp/comics-index.iso . $DIR_EXTRACT index-pages/=/$DIR_PROJECT
cdrecord -v -eject dev=$DEV_RECORDER -data /tmp/comics-index.iso

rm -r $DIR_PROJECT
rm -r $DIR_EXTRACT
rm -f /tmp/comics-index.iso
;;
*)
exit 1
;;
esac

Para poder ejecutar el script es necesario tener previamente instalado el paquete ImageMagick. Se trata de un conjunto de programas que permiten manipular imagenes en diversos formatos desde la línea de comandos. El script utiliza el paquete ImageMagick para redimensionar imágenes y convertirlas de un formato a otro obteniendo así las miniaturas de las portadas que luego se incluyen en los índices HTML.

La verdad es que el código HTML que genera el script es bastante simple. Espero que alguno de los que esté leyendo estas líneas se anime a proponer mejoras para lograr índices con un aspecto visual más cuidado.

Un par de detalles que he observado para que el script funcione correctamente son los siguientes:

  • Para habilitar el autoarranque del CD en Linux es necesario tener instalado el gnome-volume-manager, y asegurarse de tener activada la opción "Auto ejecutar programas en los soportes y unidades nuevos" dentro del menú "Preferencias > Unidades y soportes extraíbles". Además, hay que asegurarse de que el lector de CD-ROM no esté siendo montado con la opción "noexec" ya que en ese caso no podríamos ejecutar nada que esté dentro del CD.
  • En segundo lugar, avisar que para que los índices se vean correctamente con el Internet Explorer si estáis en Windows, se debe seleccionar "Ver > Codificación > Unicode (UTF-8)".

Podéis descargar el script completo siguiendo este enlace. Una vez descargado debéis darle los permisos de ejecución

martes 21 de agosto de 2007

Programa en .NET para controlar el PC desde un mando a distancia (IV): Diseñar la interfaz de usuario

En las últimas entregas he tratado de explicar el funcionamiento de las clases que iban a ser utilizadas por el programa cliente para WinLIRC. En esta nueva entrega veremos el diseño de la aplicación que hace uso de dichas clases.

Seguir Leyendo...

Dado que la clase "ClienteTCP" es de carácter más genérico, decidí compilarla como una biblioteca de clases en una DLL a parte, y luego incluir en el proyecto de la aplicación principal una referencia a dicha DLL. Para ello solo hay que abrir la sección de referencias dentro de las propiedades del proyecto y añadir la ruta a la DLL previamente compilada.

El formulario principal de la aplicación tendrá el siguiente aspecto.

Dentro del código asociado al formulario principal debemos incluir las siguientes declaraciones de objetos.

Private WithEvents WinSockCliente As New TcpClientClass.ClienteTCP
Private ConfiguracionTeclas As New ConfiguracionTeclas
#If DEBUG Then
Private Sw As StreamWriter
#End If

Las dos primeras declaraciones corresponden a objetos de las clases "ClienteTCP" y "ConfiguracionTeclas" que ya han sido explicadas en las anteriores entregas. La tercera declaración está metida dentro de un bloque de compilación condicional, por lo que solo será efectiva cuando el proyecto se compila en modo debug, cuando se compila en modo release es como si dicha línea no existiera. Más adelante se verá que esta tercera declaración es necesaria para el mantenimiento de un fichero de log con propósitos de debug.

Lo primero que hace la aplicación al cargar el formulario principal es rellenar los textbox de configuración a partir de la información que tiene almacenada en una serie de claves del registro de windows. Además, si la aplicación se ha compilado en modo debug se crea un fichero llamado "Debug.log" y se obtiene un stream a través del cual escribir en dicho fichero. 

Private Sub frmClienteWinLIRC_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim ClaveLeida As String

ClaveLeida = Registry.GetValue("HKEY_LOCAL_MACHINE\Software\Cliente WinLIRC", "IPdelHost", Nothing)
If ClaveLeida IsNot Nothing Then
txtIP.Text = ClaveLeida
End If
ClaveLeida = Registry.GetValue("HKEY_LOCAL_MACHINE\Software\Cliente WinLIRC", "PuertodelHost", Nothing)
If ClaveLeida IsNot Nothing Then
txtPuerto.Text = ClaveLeida
End If
ClaveLeida = Registry.GetValue("HKEY_LOCAL_MACHINE\Software\Cliente WinLIRC", "ArchivoConfig", Nothing)
If ClaveLeida IsNot Nothing Then
txtArchivoConfig.Text = ClaveLeida
End If
#If DEBUG Then
Sw = New StreamWriter("Debug.log")
#End If
End Sub

El textbox en el que se indica el archivo de configuración que se va a usar (el que asocia los nombres de los botones del mando a distancia con las combinaciones de teclas enviadas a la aplicación a controlar) es de solo lectura, es decir, no se puede editar directamente. Para seleccionar un archivo de configuración hay que pulsar el botón "Cambiar..." que hay a la derecha del textbox. Esto provoca que se visualice uno de los cuadros de dialogo usados para abrir archivos. En este tipo de cuadros de dialogo podemos navegar por las carpetas que contiene nuestro equipos y seleccionar el archivo que queramos. Al salir del cuadro de dialogo el nombre del archivo seleccionado aparece en el textbox.

Private Sub btnSeleccionarArchivo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnSeleccionarArchivo.Click
If OpenFileDialog.ShowDialog() = Windows.Forms.DialogResult.OK Then
txtArchivoConfig.Text = OpenFileDialog.FileName
End If
End Sub

Cuando se pulsa el botón "Conectar" del formulario, lo primero que se hace es utilizar el método "LeerConfiguración" de la clase "ConfiguracionTeclas" pasándole el nombre del archivo de configuración que hemos seleccionado. Si la configuración se lee correctamente, se cargan las propiedades "IPDelHost" y "PuertoDelHost" de la clase "ClienteTCP" con los datos que hayamos escrito en los textbox correspondientes, y se llama al método "Conectar" de dicha clase para realizar la conexión con el servidor WinLIRC. Además, se deshabilitan los textbox de configuración y el botón de "Conectar" y se habilita el botón de "Desconectar". Si al intentar leer el archivo de configuración se produce un error se indicará mediante un mensaje.

Private Sub btnConectar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConectar.Click
Dim RetVal As Boolean

RetVal = ConfiguracionTeclas.LeerConfiguracion(txtArchivoConfig.Text)
If RetVal = True Then
txtIP.Enabled = False
txtPuerto.Enabled = False
btnConectar.Enabled = False
btnDesconectar.Enabled = True
With WinSockCliente
.IPDelHost = txtIP.Text
.PuertoDelHost = txtPuerto.Text
.Conectar()
End With
Else
MsgBox("No se ha podido abrir el archivo de configuración", MsgBoxStyle.Critical, "Error")
End If
End Sub

Como vimos en la entrega en la que se explico la clase "ClienteTCP", la llamada al método "Conectar" inicia un nuevo Thread que se ejecuta en segundo plano y se encarga de recibir los datos procedentes del servidor WinLIRC. Cada vez que llegan datos la clase "ClienteTCP" lanza un evento llamado "DatosRecibidos". El código de la función que maneja dicho evento sería el siguiente.

Private Sub WinSockCliente_DatosRecibidos(ByVal Datos As Byte()) Handles WinSockCliente.DatosRecibidos
Dim CadenaRecibida As String
Dim i As Integer

For i = 0 To Datos.Length
If Datos(i) = &HA Then
Exit For
End If
Next
CadenaRecibida = Encoding.ASCII.GetString(Datos, 0, i)
If Me.InvokeRequired Then
Dim Method As New DelegateProcesarDatos(AddressOf ProcesarDatosRecibidos)
Me.BeginInvoke(Method, New Object() {CadenaRecibida})
Else
ProcesarDatosRecibidos(CadenaRecibida)
End If
End Sub

Para entender el código hay que saber que la función que maneja el evento se ejecuta en el contexto del segundo Thread, y para evitar conflictos entre múltiples Threads no podremos acceder desde esta función a los controles del formulario. Lo que debemos hacer es usar un delegado. La verdad es que esta parte de las aplicaciones multithread es un poco liosa. Podéis encontrar una explicación más detallada de por qué he usado un delegado en este artículo.

Una vez entendido el uso de los delegados podréis ver que, a grandes rasgos, lo que hace la función que maneja el evento es, convertir la secuencia de bytes en ASCII recibidos desde el servidor WinLIRC (que termina con un byte 0x0A) en un string Unicode, y luego llamar a la función "ProcesarDatosRecibidos" a través de un delegado de forma que dicha función se ejecutará ya en el contexto del hilo principal y por tanto se podrá acceder desde ella a los controles del formulario. A la función "ProcesarDatosRecibidos" se le pasa el string que ha llegado desde el servidor WinLIRC. Veamos ahora el código de dicha función.

Private Sub ProcesarDatosRecibidos(ByVal CadenaRecibida As String)
Dim CadenaDividida() As String

CadenaDividida = CadenaRecibida.Split(" ")
If (CadenaDividida(1) = "00") Then
ConfiguracionTeclas.EnviarOrden(CadenaDividida(2))
End If
#If DEBUG Then
Sw.WriteLine(CadenaRecibida)
#End If
End Sub

Recordemos que un ejemplo de mensaje recibido desde el servidor WinLIRC podría ser el siguiente:

0000000000001335 00 play UNIVERSAL-CODE559

Dentro del mensaje, en la tercera posición, aparece el nombre del botón del mando a distancia que se ha pulsado. Además, si el botón del mando a distancia se mantiene pulsado se irán recibiendo mensajes periódicamente hasta que el botón deje de pulsarse. Estos mensajes se distinguen unos de otros en el número que aparece en la segunda posición.

La función "ProcesarDatosRecibidos" utiliza el método "Split" para separar las diferentes partes del mensaje (divide el string utilizando como separadores los espacios en blanco). Para evitar repetir la misma orden varias veces al mantener pulsado un botón, primero se comprueba que el número que aparece en la segunda posición del mensaje sea cero, o lo que es lo mismo, que es el primer mensaje recibido al pulsar el botón y no una de las repeticiones sucesivas. En tal caso utiliza el método "EnviarOrden" de la clase "ConfiguracionTeclas" pasándole como parámetro el nombre del botón pulsado en el mando. Como ya sabemos, esto dará lugar al envío de la combinación de teclas que corresponda hacia la aplicación que se quiere controlar.

Al final de la función hay una parte que solo se tiene en cuenta cuando la aplicación se compila en modo debug y que se encarga de escribir en el fichero "Debug.log" cada uno de los mensajes recibidos desde el servidor WinLIRC. Este tipo de ficheros de log resultan útiles durante la etapa de depuración del programa y no aparecerán cuando el programa sea compilado en modo release.

Si en algún momento se quiere cortar la conexión con el servidor WinLIRC bastará con pulsar el botón "Desconectar" del formulario principal. Internamente se llamará al método "Desconectar" de la clase "ClienteTCP".

Private Sub btnDesconectar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDesconectar.Click
WinSockCliente.Desconectar()
End Sub

Como ya vimos, tras finalizar la conexión, la clase "ClienteTCP" lanza el evento "ConexionTerminada". El código de la función que maneja el evento es el siguiente.

Private Sub WinSockCliente_ConexionTerminada() Handles WinSockCliente.ConexionTerminada
If Me.InvokeRequired Then
Dim Method As New DelegateHabilitarConexion(AddressOf HabilitarConexion)
Me.BeginInvoke(Method)
Else
HabilitarConexion()
End If
End Sub

Al igual que hacíamos en el manejo del evento "DatosRecibidos", en este caso se invoca la función "HabilitarConexión" a través de un delegado. Dicha función se encarga de volver a habilitar los textbox de configuración y el botón de "Conectar" y de deshabilitar el botón de "Desconectar", quedando así el programa preparado para establecer una nueva conexión con el servidor WinLIRC cuando el usuario lo desee.

Private Sub HabilitarConexion()
txtIP.Enabled = True
txtPuerto.Enabled = True
btnConectar.Enabled = True
btnDesconectar.Enabled = False
End Sub

Por cierto, hay que recordar incluir en el código, a continuación de la declaración de variables, la declaración de los dos delegados que hemos usado.

Delegate Sub DelegateProcesarDatos(ByVal Cadena As String)
Delegate Sub DelegateHabilitarConexion()

Con objeto de poder tener el cliente para WinLIRC funcionando siempre que queramos y que estorbe lo mínimo posible a nuestro trabajo, cuando se minimiza el formulario principal desaparece de la barra de tareas quedando solo un icono en la bandeja del sistema. Al hacer click con el botón derecho sobre el icono se despliega un pequeño menú con las siguientes opciones. 

  • Configuración: Muestra nuevamente la ventana principal de la aplicación.
  • Acerca de: Muestra una pequeña ventana con información sobre la versión y el autor de la aplicación.
  • Salir: Cierra la aplicación.

Para que al ejecutarse la aplicación aparezca un icono en la bandeja del sistema necesitamos añadir al formulario principal un control de tipo "NotifyIcon", y para el menú contextual que aparece al hacer click con el botón derecho sobre dicho icono añadiremos un control de tipo "ContextMenuStrip". Por medio de la propiedad "Icon" del control "NotifyIcon" seleccionaremos el icono que queremos que aparezca en la bandeja. Además, en la propiedad "ContextMenuStrip" del control "NotifyIcon" deberemos indicar el nombre que hemos dado al control "ContextMenuStrip" previamente añadido y configurado. El código que se ejecuta al hacer click sobre alguna de las opciones del menú contextual es el siguiente.

Private Sub ConfiguraciónToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ConfiguraciónToolStripMenuItem.Click
If Me.WindowState = FormWindowState.Minimized Then
Me.WindowState = FormWindowState.Normal
Me.ShowInTaskbar = True
End If
Me.Activate()
End Sub

Private Sub SalirToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles SalirToolStripMenuItem.Click
Me.Close()
End Sub

Private Sub AcercaDeToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles AcercaDeToolStripMenuItem.Click
frmAcercaDe.ShowDialog()
End Sub

Para conseguir que la aplicación desaparezca de la barra de tareas al minimizar el formulario principal necesitamos además añadir el siguiente código.

Private Sub frmClienteWinLIRC_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Resize
If Me.WindowState = FormWindowState.Minimized Then
Me.ShowInTaskbar = False
End If
End Sub

Al cerrar la aplicación hay que borrar el icono de la bandeja del sistema y liberar los recursos empleados por los controles "NotifyIcon" y "ContextMenuStrip". Además, antes de cerrarse, el programa aprovecha para guardar los contenidos de los textbox de configuración en varias claves del registro de Windows. Como ya hemos visto, la próxima vez que el programa se inicie leerá la información guardada en el registro, y de esa forma no será necesario tener que rellenar los textbox cada vez que arranquemos la aplicación. Si la aplicación ha sido compilada en modo debug también debe cerrarse el fichero "Debug.log" antes de terminar.

Private Sub frmClienteWinLIRC_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
IconoBandeja.Visible = False
IconoBandeja.Dispose()
MenuIconoBandeja.Dispose()
Registry.SetValue("HKEY_LOCAL_MACHINE\Software\Cliente WinLIRC", "IPdelHost", txtIP.Text, RegistryValueKind.String)
Registry.SetValue("HKEY_LOCAL_MACHINE\Software\Cliente WinLIRC", "PuertodelHost", txtPuerto.Text, RegistryValueKind.String)
Registry.SetValue("HKEY_LOCAL_MACHINE\Software\Cliente WinLIRC", "ArchivoConfig", txtArchivoConfig.Text, RegistryValueKind.String)
#If DEBUG Then
Sw.Close()
#End If
End
End Sub

Con esto finalizan las cuatro entregas dedicadas al cliente para WinLIRC, y al control de aplicaciones desde un mando a distancia de infrarrojos. Espero que lo explicado aquí sea de utilidad para alguno de los que estáis leyendo estas líneas y que os animéis a escribir comentarios planteando dudas o posibles mejoras.

A continuación tenéis los enlaces para descargar tanto el proyecto completo como el instalador de la aplicación:

Proyecto completo para Visual Studio 2005: Descargar.

Instalador de la aplicación: Descargar.