índice de vb.net

Cómo crear Threads en vb.Net

y, (como siempre), algunas cosillas más...

Actualizado: 27/Mar/2001


Nota aclaratoria:
El código mostrado aquí era para BETA1 de Visual Basic .NET, por tanto no es válido para la versión definitiva. Si quieres ver una versión de este ejemplo usando la versión "final" de Visual Studio .NET, pulsa en este link:
Cómo... Usar Threads en Visual Basic .NET


Una de las nuevas características que podemos encontrar en la próxima versión de Visual Basic 7.0 (o vb.Net) es que podemos crear diferentes Threads (o hilos), para que nuestra aplicación pueda hacer varias cosas a un mismo tiempo.

Para ver cómo funciona esto de los Threads, he creado un ejemplo que busca en un path determinado los ficheros con la extensión que le indicamos y asigna a una colección cada una de las palabras que encuentra, una vez procesados todos los ficheros, muestra en un ListView cada una de las palabras así como el número de veces que está dicha palabra en todos los ficheros procesados.
Para cada directorio a procesar, se usará un Thread diferente.

Te relaciono a continuación las otras cosillas que podrás ver en el código de ejemplo:
Al final de esta página encontrás un link con el código completo.


¡Espero que disfrutes de lo que hay en esta página!

Nos vemos.
Guillermo


Crear y usar Threads, los conceptos básicos:

Todas las aplicaciones se ejecutan en un Thread (o hilo de ejecución). Pero cada aplicación puede tener más de un Thread al mismo tiempo, es decir se pueden estar haciendo varias cosas a un mismo tiempo. En Visual Basic.Net, a diferencia de las versiones anteriores, se pueden crear Threads para que podamos realizar diferentes tareas a un mismo tiempo, el uso o no de Threads lo decidirás tú, ya no es algo que no podamos hacer aunque quisieramos...

Cuando se define un nuevo Thread, lo que hay que hacer es indicarle al compilador cual será el porcedimiento que queremos usar de forma paralela al resto de la aplicación. Este procedimiento debe ser obligatoriamente del tipo SUB y además, al menos en la Beta1, no admite parámetros.

Veamos de forma simple lo que necesitamos para poder "thredear" en nuestras aplicaciones:

En la clase en la que queramos lanzar un Thread, deberemos hacer un Imports System.Threading, crear una variable de tipo Thread y asignarle el procedimiento que queramos ejecutar en dicho Thread, (que será un Sub de otra clase u objeto):

' Usamos el Imports para que podamos crear Threads
Imports System.Threading

' Creamos una variable del tipo Thread
Private mThreadFic As Thread

' Creamos una variable de la clase en la que está el Sub que se usará en el Thread
Private mProcesarFic As cProcesarFicheroThread

' Asignamos el Sub que queremos usar al crear una nueva instancia de la clase del tipo Thread
mThreadFic = New Thread(New ThreadStart(AddressOf mProcesarFic.ProcesarDir))

' Para que se ejecute el Thread, hay que indicarselo de forma explícita
mThreadFic.Start()

Cuando un Thread se inicia, con Start, se continua ejecutando el código que sigue y así podremos seguir usando nuestra aplicación de forma paralela al Thread que está en ejecución. Por supuesto se pueden crear y ejecutar varios Threads a un mismo tiempo y cada uno de ellos hará su trabajo de forma independiente.
Puede ser que en algunas ocasiones necesitemos saber si un Thread aún se está ejecutando, para ello se puede usar la propiedad IsAlive del objeto Thread:
If mThreadFic.IsAlive() Then
Aunque esto último sólo podremos usarlo dentro de un bucle de espera... con lo cual nuestra aplicación se quedaría "parada" mientras comprueba si está o no "vivo" el Thread en cuestión... de esta forma, nos tendríamos que plantear si realmente necesitamos usar Threads cuando debemos esperar a que termine... la verdad es que no sería demasiado útil, aunque, como casi en todo, siempre hay sus excepciones...
En el código de ejemplo que mostraré más abajo tenemos una de estas excepciones, ya que de lo que se trata es de procesar los ficheros de varios directorios a un mismo tiempo para recopilar información y hasta que todo el proceso no ha terminado no podemos mostrar los datos procesados.
En dicho ejemplo también veremos cómo usar un array para manejar varios Threads a un mismo tiempo.

Nota:
En el código mostrado en este artículo, los Threads llaman a procedimientos de otra clase, pero un Thread puede llamar a un procedimiento de la propia clase en la que se crea el Thread. Para ver el ejemplo de esto que digo, échale un vistazo al
código "alternativo" mostrado al final.

Volver arriba


Crear array de Threads:

En el código del proyecto de ejemplo que acompaña a este artículo, podrás ver que se pueden usar array de Threads que a su vez acceden a un array de la clase que será llamado por cada Thread. Esto he tenido que hacerlo así por la sencilla razón de que cada Thread debe llamar a una clase diferente para que cada una de ellas procese un directorio diferente. Si no lo hubiese hecho así, todos los Threads hubiesen llamado a la misma instancia de la clase... y eso no es operativo, ya que harían varias veces el trabajo sobre el mismo directorio.

El problema surge cuando necesitamos un solo contador del número de directorios procesados.
Además, la clase produce un evento cada vez que se procesa cada uno de los ficheros de cada directorio, (cuantos cadas ¿verdad?), así como otro evento cuando todo ha terminado y como quiera que la clase está declarada como un array, y los arrays de clases no pueden declararse con WithEvents, que sería lo que necesitaríamos para controlar los eventos producidos.. nos encontramos con otro inconveniente.
La suerte es que todo esto no son inconvenientes ni problemas insalvables, ya que tanto las propiedades, métodos como los eventos de una clase pueden declararse como compartidos, (usando Shared). Esto quiere decir que todas las instancias (copias) de la clase usarán siempre el mismo procedimiento compartido.

Aquí te muestro parte del código usado, el cual podrás ver al completo un poco más abajo:

' En la clase que será llamada por cada Thread
Public Shared Event ProcesandoFichero(ByVal sMsg As String)
'
Public Shared TotalFicheros As Integer = 0


' En la clase que creará los Threads:

' Este será el array de clases
Private mProcesarFic() As Guille.Clases.cProcesarFicheroThread
' Esta será la clase que se usará para recibir los eventos:
Private WithEvents mProcesarFic2 As Guille.Clases.cProcesarFicheroThread
' Array para cada uno de los threads
Private mThreadFic() As Thread


' Este será el código que se usará para asignar cada uno de los Threads:

' Crear un thread para cada directorio a procesar
nThread += 1
ReDim Preserve mThreadFic(nThread)
ReDim Preserve mProcesarFic(nThread)
mProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras)
mProcesarFic(nThread - 1).sDir = sDir
mProcesarFic(nThread - 1).sExt = sExt
' Asignamos al Thread el procedimiento de la clase recién creada
mThreadFic(nThread - 1) = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir))
'
' Iniciar el thread
mThreadFic(nThread - 1).Start()

Como puedes ver, la variable que controla los arrays, se incrementa en uno y se redimensionan los arrays, el detalle es que al dimensionar un array, el valor que se indica es el número de elementos que tendrá dicho array, pero al empezar a contar desde cero, el valor máximo que aceptará será el número de elementos menos uno, por eso se usa: nThread - 1 dentro de los paréntesis... seguramente ya lo sabías, pero he creido conveniente aclararlo...

Por último, elprocedimiento que interceptará cada evento producido por cada uno de los Threads; éste a su vez producirá otro evento que será interceptado por el formulario.

' Este sería el procedimiento al que llamarán todos los eventos producidos por cada uno de los Threads,
' a pesar de que el nombre de la clase sea diferente.
Private Sub mProcesarFic2_ProcesandoFichero(ByVal sMsg As String)
	' Este evento se produce cuando se está procesando un fichero
	' por uno de los threads.
	' Al declararse como Shared se ejecutará
	' aunque el nombre de la clase sea diferente.
	' Desde aquí producimos nuestro evento a la aplicación.
	RaiseEvent ProcesandoFichero(sMsg)
End Sub

Volver arriba


Procesar todos los subdirectorios de un directorio:

Usando la clase Directory de System.IO podemos asignar a un array del tipo Directory todos los subdirectorios de un directorio (o path) determinado, para ello crearemos un array del tipo Directory y usaremos el método GetDirectoriesInDirectory del objeto Directory:

Dim tDir As Directory
Dim tSubDirs() As Directory

tSubDirs = tDir.GetDirectoriesInDirectory(sDir)

Una vez que tenemos asignado el array podemos procesar cada uno de los subdirectorios, haciendo un bucle:

For Each tDir In tSubDirs
	' Procesar cada uno de los directorios
	'
	' tDir.FullName nos dará el path completo
Next

La función GetDirectoriesInDirectory devuelve un array con todos los subirectorios del path indicado en el parámetro.
Para poder recorrer todos los subdirectorios, habría que comprobar si cada uno de los subdirectorios tienen a su vez más subdirectorios... lo mejor es usar un procedimiento recursivo, en
el código de ejemplo puedes ver cómo hacerlo.

Volver arriba


Procesar todos los ficheros de un directorio:

Cuando se trabaja con ficheros, lo habitual es poder acceder a todos los que estén dentro de un directorio (o path) determinado, para poder asignar todos los ficheros de un directorio en un array, podemos usar la función GetFilesInDirectory de la clase Directory:

'
Dim tDir As Directory
Dim tFiles() As File

tFiles = tDir.GetFilesInDirectory(sDir, sExt)

For i = 0 To tFiles.Length - 1

Al igual que la función mostrada en el punto anterior, podemos hacer un bucle con los elementos del array, la única diferencia es que en esta ocasión se trata de un array de objetos del tipo File y tanto podemos hacer el bucle con For i = 0 To Número_de_elementos - 1 como con el For Each mostrado en el punto anterior.
También podemos saber el número de elementos a procesar en el bucle usando Ubound(tFiles):
For i = 0 To Ubound(tFiles)

Volver arriba


Acceder a ficheros: guardar y leer, (usando el formato estándar UTF8), con StreamWriter y StreamReader:

El .NET Framework utiliza diferentes tipos de acceso a ficheros. El que trae por defecto suele ser el llamado UTF8, que aunque si se abre con el Notepad puda parecer un fichero normal y corriente, no lo es, (al menos esa es la impresión que a mi me da, después de varias pruebas que he realizado, sobre todo al ver cómo caracteres "especiales" como puedan ser la eñe y las vocales acentuadas (o con tilde), se muestran de forma errónea.
A la hora de leer de un fichero, podemos indicarle el formato que queramos que tenga: ANSI, ASCII, UTF7, UTF8, etc. Pero al guardar los datos sólo he podido usar el formato UTF8.
¿Cual es el problema? Ninguno si se usa el mismo formato para guardarlo como para leerlo.
El problema viene cuando queremos acceder a otros ficheros que ya tengamos en nuestro disco duro... que esos seguramente estarán guardados con el formato "predeterminado" de Windows: ANSI.

Pero eso será tema de otro artículo, (al menos cuando de con la solución perfecta para saber de que tipo de fichero se trata).

Veamos cómo guardar datos en un fichero:
Para esto necesitaremos dos objetos: uno de tipo File para poder crear el fichero y otro del tipo StreamWriter para poder guardar la información en el fichero recién creado.

'
Dim tFile As IO.File
Dim sFile As IO.StreamWriter
'
sFile = tFile.CreateText(sFileName)
' Guardar la identificación de que es un fichero de Palabras
sFile.WriteLine("cPalabras")
' Guardar el número de elementos
sFile.WriteLine(m_Palabras.Count)
' Guardar cada una de las palabras
For Each tPalabra In m_Palabras
	With tPalabra
		sFile.WriteLine(.ID)
		sFile.WriteLine(.Veces)
	End With
Next
sFile.Close()

El método WriteLine guarda también los caracteres de fin de línea.

Ahora vamos a ver cómo leer datos de un fichero:
En esta ocasión también vamos a usar dos objetos, uno de ellos del tipo File, (el cual usaremos para abrir el fichero), y el otro será del tipo StreamReader que lo usaremos para leer la información del fichero.

'
Dim s As String
Dim tFile As IO.File
Dim sFileReader As IO.StreamReader
'
' Según dice la ayuda:
' OpenText: Creates a StreamReader with UTF8 encoding that reads from an existing text file.
sFileReader = tFile.OpenText(sFic)
'
' Leer cada una de las palabras
' Si .Peek = -1 es que ya no hay nada que leer,
' esto es como el .EOF del VB6
Do While Not sFileReader.Peek = -1
	' Asignar en s la línea leida, sin espacios extras
	s = sFileReader.ReadLine.Trim
	' Si no es una línea vacia...
	If s.Length > 0 Then
		' ... lo que haya que hacer con la línea leida ...
	Else
		' Si el ID era una cadena vacía, leer el siguiente dato
		s = sFileReader.ReadLine
	End If
Loop
sFileReader.Close()

El método ReadLine lee una línea completa, es como el Line Input# del VB6.
Para comprobar si aún hay datos que leer, he usado la función Peek, si ésta devuelve un valor -1, es que ya no hay más datos en el fichero, es como si se usara EOF() en el Visual Basic 6.

Volver arriba


Leer en un array del tipo Char el contenido de un fichero ANSI usando StreamReader:

Ya te he comentado antes que las funciones existentes en el .NET Framenwork para acceder a los ficheros usa la encodificación (¿se dice así?) del tipo UTF8; pero los ficheros creados con Windows 9x e incluso con el Windows 2000 es del tipo ANSI, así que si queremos leer ficheros en los que haya letras especiales para los anglosajones: eñes, acentos, etc., tendremos que especificarlo, al menos a la hora de leer los datos, ya que, como te he comentado anteriormente, para escribir, sólo he podido hacerlo usando el formato UTF8.

Para poder indicarle al "sistema" que queremos leer en el formato ANSI del código de página del Windows, tendremos que indicárselo mediante el método: .SwitchEncoding del objeto StreamReader. Realmente la forma completa sería: .SwitchEncoding(System.Text.Encoding.Default)

Veamos un ejemplo:

Dim tDir As Directory
Dim tFiles() As File
Dim i, n As Integer
Dim s() As Char
Dim sFile As IO.StreamReader
'
' Asigna a tFiles un array con los nombres de los ficheros,
' path incluido
tFiles = tDir.GetFilesInDirectory(sDir, sExt)
'
For i = 0 To tFiles.Length - 1
	'
	sFile = tFiles(i).OpenText()
	With sFile
		' Esto funcionará sólo si NO se ha guardado con formato UTF8
		.SwitchEncoding(System.Text.Encoding.Default)
		ReDim s(tFiles(i).Length.ToInt32 + 1)
		.Read(s, 1, tFiles(i).Length.ToInt32)
		.Close()
	End With
	'
	' Lo que haya que hacer con el contenido del array
	' ...
Next

El método Read del objeto StreamReader asigna a un array del tipo Char los caracteres que le indiquemos: desde que posición y cuantos.

Volver arriba


Crear una clase/colección para almacenar las palabras halladas:

El fichero cPalabras.vb contiene varias clases para crear los objetos usados para almacenar cada palabra en la colección.
En este fichero he creado en total 4 clases, una de ellas es realmente la colección. No son necesarias tantas clases, realmente con dos hubiese sido suficiente: una para cada una de las palabras y la otra para la colección.

La primera clase (cID) es para implementar una propiedad ID, que usaré para el índice o nombre de cada palabra encontrada.
La segunda clase (cContenido), hereda la clase cID, además de implementar la propiedad Contenido.
La tercera (cPalabra), implementa la clase cContenido, que a su vez hereda la propiedad ID de la clase cID. Como sabrás en Visual Basic.NET (y también en c#) sólo se puede heredar una clase al mismo tiempo, pero al usar clases que a su vez heredan otras clases... pues podemos conseguir un maremagnun de clases heredadas... o casi.
Por último, la cuarta clase es la colección: cPalabras.

Todas las propiedades y métodos de estas clases están declarados con Overridable, de esta forma permitimos que las clases que hereden a estas clases puedan "sobreescribir" los métodos para usar la implementación que el autor quiera. Esto de usar métodos Overridable le añade un poco de más sobrecarga a las clases, es decir, si creemos que los métodos que implementamos no necesitarán sobreescribirse, mejor sería no usar este modificador.

Volver arriba


Usar ArrayList y HashTable para crear nuestras propias colecciones:

En el código de la colección cPalabras, he usado un ArrayList para contener los objetos (cPalabra) que manipulará la colección y un array del tipo HashTable para poder acceder a un elemento sabiendo el ID del mismo. Los ArrayList permiten acceder mediante un índice numérico o bien mediante un objeto de los que están almacenados en dicho array, pero no permiten acceder mediante un índice de tipo cadena, cosa que si se puede hacer con los HashTables.

Esto se ve en el código del método Exists de la colección:

Public Overridable Function Exists(ByVal sID As String) As Boolean
	' Comprueba si el ID indicado existe en la colección    (11/Oct/00)
	' Devuelve verdadero o falso, según sea el caso
	Return m_ht.ContainsKey(sID)
End Function

En los ArrayList existe un método Contains, pero el parámetro que hay que suministrar es del tipo Object y lo que hace es comprobar si dicho objeto está o no en la colección, por otro lado el método ContainsKey del objeto HashTable localiza un objeto que tenga la clave indicada.

¿Por qué usar dos tipos de objetos diferentes para hacer una misma cosa?
Básicamente porque los ArrayList permiten iterar por los objetos que contienen con For Each y permite que se pueda acceder a un elemento por el índice dentro del array. Por otro lado HashTable permite buscar y acceder a los elementos por medio de un valor clave, al estilo del objeto Dictionary del VB6.
Seguramente no será la mejor forma de crear una colección, pero...

Volver arriba


Usar eventos en nuestras clases:

Los eventos se crean y se acceden en Visual Basic.Net de la misma forma que en VB6, la única diferencia es que en las versiones anteriores de Visual Basic (5 ó 6) se pueden ver los eventos que cada objeto (o clase declarada con WithEvents) expone. En Visual Basic.Net hay que intuirlo o con un poco de suerte (y después de buscar un poquito) encontrarlos.
Como pista te diré que por regla general los nombres de los procedimientos de los eventos se forman de esta forma:
El nombre del objeto un guión bajo y el nombre del evento: Private Sub mProcesarFic_ProcesandoFichero()
Espero que en la versión definitiva sea más fácil saber que eventos se pueden usar de cada clase.

Veamos un ejemplo:

' El evento que producirá esta clase
Public Event ProcesandoFichero(ByVal sMsg As String)


' Para usar los eventos de una clase hay que declararla con WithEvents:
Private WithEvents mProcesarFic As Guille.Clases.cProcesarFicheroThread


' El procedimiento que será llamado cuando se produzca un evento:
Private Sub mProcesarFic_ProcesandoFichero(ByVal sMsg As String)
    '...
End Sub

Aunque también se pueden crear de esta otra forma:
El_nombre_que_queramos_usar Handles Objeto.Nombre_del_evento:
Private Sub EventoProcesandoFic() Handles mProcesarFic.ProcesandoFichero
Es decir, podemos llamar al procedimiento como más nos guste, aunque hay que usar los parámetros que el evento original tenga y añadir al final el evento que deberá manejar. Esto último se hace con Handles y el nombre del objeto declarado con WithEvents seguido del evento a manejar.
La ventaja es que podemos manejar varios eventos en un mismo procedimiento, para ello se indicarán después de Handles separados por comas.
Supongamos que tenemos dos objetos que producen eventos:

Private WithEvents MiObjeto1 As UnObjeto
Private WithEvents MiObjeto2 As UnObjeto

' Este procedimiento manejará los eventos de los dos objetos
Private Sub manejarEvento(ByVal unaCadena As String) Handles MiObjeto1.unEvento, MiObjeto2.unEvento
    '...
End Sub

Eventos en array de clases:
Habrá ocasiones en las que nos interese usar una clase que produzca eventos como si fuese un array, en esas ocasiones no nos estará permitido crear un array con WithEvents, por tanto "aparentemente" no podremos crear eventos, pero la cosa no es así, ya que si el evento lo declaramos para que sea compartido por todas las copias de nuestra clase, (usando Shared), ese evento se generará incluso aunque se produzca en una de las instancias del array; aunque para ello tendremos que crear una segunda variable simple declarada con WithEvents.
En
el código completo del ejemplo puedes ver cómo hacerlo.

Volver arriba


Usar diálogos comunes para seleccionar un fichero leer y guardar:

En las versiones anteriores de Visual Basic, se suele usar un mismo control ActiveX (OCX) para mostrar los cuadros de diálogo de Abrir y Guardar ficheros, pero en Visual Basic.Net existen objetos diferentes para cada una de estas acciones.
El objeto que se usa para mostrar el diálogo de Abrir ficheros es: OpenFileDialog y el que se usa para guardar es: SaveFileDialog.
En este último objeto, si el fichero elegido ya existe, pregunta si se quiere sobreescribir, todo un detalle.

Veamos cómo usarlos:

' Leer de un fichero y asignarlo a la colección de palabras
With FD
	.Filter = "Textos (*.txt)|*.txt|Todos (*.*)|*.*"
	.FileName = Application.StartUpPath & "\Palabras.txt"
	If .ShowDialog() = WinForms.DialogResult.OK Then



' Seleccionar el fichero en el que se guardará
With SFD
	.Filter = "Textos (*.txt)|*.txt|Todos (*.*)|*.*"
	.FileName = System.WinForms.Application.StartUpPath & "\Palabras.txt"
	If .ShowDialog() = WinForms.DialogResult.OK Then

Con System.WinForms.Application.StartUpPath o simplemente Application.StartUpPath podemos saber el directorio desde el que se ejecuta nuestra aplicación. A diferencia del control de las versiones actuales de Visual Basic, el método que muestra el diálogo nos permite saber que botón es el que se ha pulsado, para ello se comprueba si el valor devuelto es uno de los valores de la enumeración WinForms.DialogResult.

Volver arriba


Añadir ToolTips a los controles de un formulario:

Para que los controles de un formulario puedan mostrar un ToolTip, habrá que añadir un objeto del tipo ToolTip. Una vez añadido, todos los objetos tendrán esa propiedad. Recuerda que en las versiones actuales, es una propiedad de cada control.

Según he leido por los grupos de noticias, en la próxima versión de Visual Studio.NET se incluirán propiedades que actualmente no están disponibles: Tag y Name.

Volver arriba


El código del proyecto de contar palabras usando Threads (WordCountThread):

 

Links a los distintos ficheros del código:

 

El código completo del proyecto está en este fichero zip: (WordCountThread.zip 17.0KB)

Volver arriba


El formulario:

'------------------------------------------------------------------------------
' Contar palabras de ficheros                                       (02/Ene/01)
'
' Revisado para acceder a ficheros con VB6 o métodos de .NET        (15/Mar/01)
' Quito el uso de VB6                                               (24/Mar/01)
' Revisado para incluir subdirectorios                              (25/Mar/01)
' Usando la clase cBuscarPalabrasEnFicheros                         (25/Mar/01)
'
' ©Guillermo 'guille' Som, 2001
'------------------------------------------------------------------------------
Option Compare Text

Imports System.ComponentModel
Imports System.Drawing
Imports System.WinForms
'
Imports System.IO

Public Class fBuscarPalabras
    Inherits System.WinForms.Form

    ' Crear la colección para almacenar las palabras
    Private m_Palabras As New Guille.Clases.cPalabras()
    Private WithEvents m_BuscarPalabras As Guille.Clases.cBuscarPalabrasEnFicheros
    '
    Public Sub New()
        MyBase.New()

        fBuscarPalabras = Me

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

        'TODO: Add any initialization after the InitializeComponent() call
        '
        '
        ' Crear la clase de BuscarPalabras                          (25/Mar/01)
        m_BuscarPalabras = New Guille.Clases.cBuscarPalabrasEnFicheros(m_Palabras)
        '
        'txtDir.Text = Application.StartupPath  ' Mostrar el path de la aplicación
        ' Asignar el path que se usó por última vez.
        txtDir.Text = GetSetting("WordCountThread", "Paths", "Examinar", "")
        ' Añadir las extensiones al combo
        With cboExtension
            .Items.Clear()
            .Items.Add("*.htm")
            .Items.Add("*.txt")
            .Items.Add("*.*")
            .Text = "*.htm"
        End With
        lblStatus.Text = ""
        '
        ' Crear las columnas del ListView y asignar las propiedades por defecto
        With ListView1
            .View = WinForms.View.Report
            .FullRowSelect = True
            .GridLines = True
            .CheckBoxes = True
            ' Crear las cabeceras
            .Columns.Clear()
            .Columns.Add("Palabra", 120, WinForms.HorizontalAlignment.Left)
            .Columns.Add("Veces", 90, WinForms.HorizontalAlignment.Right)
            .Columns.Add("Descripción", 285, WinForms.HorizontalAlignment.Left)
        End With
    End Sub
    '
    'Form overrides dispose to clean up the component list.
    Public Overrides Sub Dispose()
        MyBase.Dispose()
        components.Dispose()
    End Sub
    '
    '
'#Region " Windows Form Designer generated code "
    '
    ' NO MOSTRADO EL CÓDIGO DEL FORMULARIO
    '
    '
    Private Sub m_BuscarPalabras_ProcesandoFichero(ByVal sMsg As String)
        ' Aquí se llegará cada vez que se esté procesando un fichero
        lblStatus.Text = sMsg
        lblStatus.Refresh()
    End Sub
    '
    Private Sub m_BuscarPalabras_FicherosProcesados(ByVal sMsg As String)
        ' Aquí llegará antes de terminar de procesar todos los ficheros
        lblStatus.Text = sMsg & " Un momento mientras se asignan las palabras al ListView..."
        lblStatus.Refresh()
        '
        ' Asignar las palabras al ListView
        AsignarListView()
        '
        If m_Palabras.Count = 1 Then
            lblStatus.Text = sMsg & " Con 1 palabras."
        Else
            lblStatus.Text = sMsg & " Con " & m_Palabras.Count.ToString & " palabras diferentes."
        End If
        lblStatus.Refresh()
    End Sub
    '
    Protected Sub AsignarListView()
        '
        ' Añadir el contenido de la colección al ListView1
        Dim tPalabra As Guille.Clases.cPalabra
        Dim tItem As ListItem
        '
        With ListView1
            .ListItems.Clear()
            For Each tPalabra In m_Palabras
                tItem = .ListItems.Add(tPalabra.ID)
                tItem.SetSubItem(0, tPalabra.Veces.ToString)
                tItem.SetSubItem(1, tPalabra.Descripción)
            Next
        End With
    End Sub
    '
    Protected Sub cmdClear_Click(ByVal sender As Object, ByVal e As System.EventArgs)
        ' Borrar el ListView y las palabras en memoria              (24/Mar/01)
        '
        ListView1.ListItems.Clear()
        m_Palabras.Clear()
        lblStatus.Text = ""
        ' No es necesario crear de nuevo la clase de buscar,
        ' ya que el total de ficheros no se sigue acumulando
        'm_BuscarPalabras = Nothing
        'm_BuscarPalabras = New Guille.Clases.cBuscarPalabrasEnFicheros(m_Palabras)
    End Sub
    '
    Protected Sub cmdLeer_Click(ByVal sender As Object, ByVal e As System.EventArgs)
        ' Leer de un fichero y asignarlo a la colección de palabras
        With FD
            .Filter = "Textos (*.txt)|*.txt|Todos (*.*)|*.*"
            .FileName = Application.StartUpPath & "\Palabras.txt"
            If .ShowDialog() = WinForms.DialogResult.OK Then
                Dim tPalabra As Guille.Clases.cPalabra
                Dim sFic As String = .FileName
                Dim s As String
                Dim j As Integer
                '
                ' Usando StreamReader                               (11/Mar/01)
                Dim tFile As IO.File
                Dim sFileReader As IO.StreamReader
                '
                ' Según dice la ayuda:
                ' OpenText: Creates a StreamReader with UTF8 encoding that reads from an existing text file.
                sFileReader = tFile.OpenText(sFic)
                '
                ' Asignar el codificador de forma que pueda leer
                ' caracteres acentuados...
                ' Default: Obtains an encoding for the system's current ANSI code page
                'sFileReader.SwitchEncoding(System.Text.Encoding.Default)
                '
                ' Leer la primera línea para saber si es un fichero de mensajes
                s = sFileReader.ReadLine
                If s <> "cPalabras" Then
                    MessageBox.Show(Me, "No es un fichero con datos de Palabras", "Leer fichero de palabras", MessageBox.IconInformation)
                    sFileReader.Close()
                    Exit Sub
                End If
                ' El número de elementos
                s = sFileReader.ReadLine
                j = CInt(Val(s))
                ' Eliminar las palabras de la colección
                m_Palabras.Clear()
                ' Leer cada una de las palabras
                ' Si .Peek = -1 es que ya no hay nada que leer,
                ' esto es como el .EOF del VB6
                Do While Not sFileReader.Peek = -1
                    ' Asignar en s la línea leida, sin espacios extras
                    s = sFileReader.ReadLine.Trim
                    ' Si no es una línea vacia...
                    If s.Length > 0 Then
                        tPalabra = New Guille.Clases.cPalabra()
                        tPalabra.ID = s
                        s = sFileReader.ReadLine
                        j = CInt(Val(s))
                        tPalabra.Veces = j
                        m_Palabras.Add(tPalabra)
                    Else
                        ' Si el ID era una cadena vacía, leer el siguiente dato
                        s = sFileReader.ReadLine
                    End If
                Loop
                sFileReader.Close()
                '
                ' Añadir el contenido de la colección al ListView1
                AsignarListView()
                '
                lblStatus.Text = " Hay " & m_Palabras.Count.ToString & " palabras."
                lblStatus.Refresh()
            End If
        End With
    End Sub
    '
    Protected Sub cmdGuardar_Click(ByVal sender As Object, ByVal e As System.EventArgs)
        ' Seleccionar el fichero en el que se guardará
        With SFD
            .Filter = "Textos (*.txt)|*.txt|Todos (*.*)|*.*"
            .FileName = System.WinForms.Application.StartUpPath & "\Palabras.txt"
            If .ShowDialog() = WinForms.DialogResult.OK Then
                Dim tPalabra As Guille.Clases.cPalabra
                ' Usando StreamWriter
                Dim tFile As IO.File
                Dim sFile As IO.StreamWriter
                '
                sFile = tFile.CreateText(.FileName)
                ' Guardar la identificación de que es un fichero de Palabras
                sFile.WriteLine("cPalabras")
                ' Guardar el número de elementos
                sFile.WriteLine(m_Palabras.Count)
                ' Guardar cada una de las palabras
                For Each tPalabra In m_Palabras
                    With tPalabra
                        sFile.WriteLine(.ID)
                        sFile.WriteLine(.Veces)
                    End With
                Next
                sFile.Close()
            End If
        End With
    End Sub
    '
    Protected Sub cmdProcesar_Click(ByVal sender As Object, ByVal e As System.EventArgs)
        ' Procesar cada uno de los ficheros que tengan la extensión indicada
        ' y asignar las palabras encontradas en la colección de palabras
        ' una vez procesados los ficheros, se asignarán al ListView
        Dim s As String = m_BuscarPalabras.Procesar(txtDir.Text, cboExtension.Text, chkConSubDir.Checked = True)
        '
        'Debug.WriteLine("Ya ha vuelto de m_BuscarPalabras.Procesar")
        '
    End Sub
    '
    Protected Sub cmdExaminar_Click(ByVal sender As Object, ByVal e As System.EventArgs)
        ' Seleccionar el directorio
        With FD
            .FileName = txtDir.Text '& "\" & TextBox2.Text
            .Filter = "Páginas web (*.htm)|*.htm;*.html|Textos (*.txt)|*.txt|Todos (*.*)|*.*"
            If InStr(cboExtension.Text, ".htm") > 0 Then
                .FilterIndex = 1
            ElseIf InStr(cboExtension.Text, ".txt") > 0 Then
                .FilterIndex = 2
            Else
                .FilterIndex = 3
            End If
            .Title = "Selecciona el directorio"
            If .ShowDialog() = DialogResult.OK Then
                txtDir.Text = .FileName
                '
                ' Guardar el directorio y fichero que se ha examinado
                SaveSetting("WordCountThread", "Paths", "Examinar", .FileName)
                '
                ' Desglosar el nombre en el path y la extensión
                Dim s As String
                Dim i As Integer
                s = .FileName
                ' Buscar la extensión
                i = InStrRev(s, ".")
                If CBool(i) Then
                    ' Asignar desde el punto (incluido) hasta el final
                    cboExtension.Text = "*" & s.Substring(i - 1)
                End If
                ' Buscar el Path
                i = InStrRev(s, "\")
                If CBool(i) Then
                    ' Asignar hasta la barra (no incluida)
                    txtDir.Text = s.Substring(0, i - 1)
                End If
            End If
        End With
    End Sub
    '
    Protected Sub cmdSalir_Click(ByVal sender As Object, ByVal e As System.EventArgs)
        ' Salir
        Me.Close()
    End Sub
End Class

Volver arriba


'------------------------------------------------------------------------------
' cPalabra y cPalabras                                              (02/Ene/01)
' Clase y colección para almacenar las palabras
'
' ©Guillermo 'guille' Som, 1997-2001
'------------------------------------------------------------------------------

' Restringe la conversión implícita de datos a tipos relacionados
' además de obligar a declarar todas las variables a usar
' e impedir compilación tardía (late-binding)
Option Strict On
' Esto obliga a declarar todas las variables...
' ¿Aún hay gente que no declara las variables?
' Si se usa Option Strict On no es necesario usar Option Explicit On
'Option Explicit On
'
' Este Imports es para tener acceso a opciones de colecciones
' para GetEnumerator
Imports System.Collections

Namespace Guille.Clases
    '--------------------------------------------------------------------------
    ' cID                                                           (05/Ene/01)
    ' Esta clase se usará como base para crear IDs en las clases
    '
    ' ©Guillermo 'guille' Som, 2001
    '--------------------------------------------------------------------------
    Public Class cID
        Private sID As String
        Private bYaEstoy As Boolean
        '
        Public Overridable Property ID() As String
            Get
                Return sID
            End Get
            Set
                If Not bYaEstoy Then
                    bYaEstoy = True
                    sID = Value
                End If
            End Set
        End Property
    End Class
    '
    '--------------------------------------------------------------------------
    ' cContenido                                                    (05/Ene/01)
    ' Esta clase se usará como base para crear Contenidos e IDs en las clases
    ' No se puede heredar más de una clase
    '
    ' ©Guillermo 'guille' Som, 2001
    '--------------------------------------------------------------------------
    Public Class cContenido
        '
        ' Hereda la clase cID, desde este momento todas las propiedades y
        ' métodos de la clase heredada pueden usarse como si se hubiesen
        ' escrito en esta, (en este caso sólo es ID).
        Inherits cID
        '
        Private sContenido As String
        '
        Public Overridable Property Contenido() As String
            Get
                Return sContenido
            End Get
            Set
                sContenido = Value
            End Set
        End Property
    End Class
    '
    '--------------------------------------------------------------------------
    ' cPalabra                                                      (02/Ene/01)
    '
    ' ©Guillermo 'guille' Som, 1997-2001
    '--------------------------------------------------------------------------
    Public Class cPalabra
        ' Hereda las propiedades ID y Contenido
        ' (incluidas en la clase cContenido)
        Inherits cContenido
        '
        Public Mostrar As Boolean   ' Si hay que mostrarla
        Public Veces As Integer     ' El número de veces que está
        '
        ' Esta propiedad simplemente almacena el valor en la propiedad Contenido
        ' heredada de cContenido
        Public Overridable Property Descripción() As String
            Get
                Return Contenido
            End Get
            Set
                ' Asignar el nuevo valor
                Contenido = Value
            End Set
        End Property
        '
        ' Incrementa el valor de Veces con el número indicado,      (24/Mar/01)
        ' por defecto es 1
        Public Overridable Sub IncVeces(Optional ByVal CuantasVeces As Integer = 1)
            Me.Veces += CuantasVeces
        End Sub
        '
        Public Overridable ReadOnly Property Clone(Optional ByVal sNuevoID As String = "") As cPalabra
            Get
                ' Hacer una copia de este objeto                    (06/Oct/00)
                '
                ' Esta copia no se puede añadir a una colección
                ' que previamente contenga este objeto, salvo que se cambie
                ' el ID de la copia.
                Dim tPalabra As New cPalabra()
                '
                With tPalabra
                    ' Si se especifica un nuevo ID, asignarlo       (11/Oct/00)
                    If CBool(Len(sNuevoID)) Then
                        .ID = sNuevoID
                    Else
                        ' sino, usar el que tenía
                        .ID = Me.ID
                    End If
                    .Descripción = Me.Descripción
                    .Veces = Me.Veces
                    .Mostrar = Me.Mostrar
                End With
                '
                Return tPalabra
            End Get
        End Property
    End Class
    '

' La colección cPalabras

    '------------------------------------------------------------------------------
    ' cPalabras, colección de cPalabra                                  (02/Ene/01)
    '
    ' ©Guillermo 'guille' Som, 1997-2001
    '------------------------------------------------------------------------------
    Public Class cPalabras
        ' El valor de Contador, al ser Shared, se mantiene entre
        ' distintas instancias de la colección,
        Shared Contador As Integer
        '
        Private mNumeroPalabra As Integer = -1
        ' m_col almacenará los objetos de la colección
        Private m_col As ArrayList
        ' m_ht almacenará los IDs de la colección, uso un Hashtable
        ' ya que permite comprobar si existe el ID en la colección que es
        ' un valor de tipo cadena, mientras que los ArrayList sólo comprueba
        ' si existe un objeto de los incluidos en el array.
        Private m_ht As Hashtable
        Private m_Index As Integer
        '
        Public Overridable Function Exists(ByVal sID As String) As Boolean
            ' Comprueba si el ID indicado existe en la colección    (11/Oct/00)
            ' Devuelve verdadero o falso, según sea el caso
            Return m_ht.ContainsKey(sID)
        End Function

        Public Overridable Sub Clear()
            ' Borrar el contenido de la colección
            m_col.Clear()
            m_ht.Clear()
            Contador = 0
            mNumeroPalabra = -1
        End Sub

        Public Overridable Function GetEnumerator() As IEnumerator
            Return m_col.GetEnumerator
        End Function

        Public Overridable Sub Remove(ByVal sIndex As String)
            ' Método Remove de una colección

            ' Comprobar si existe el elemento
            If m_ht.ContainsKey(sIndex) Then
                ' Obtener el índice dentro de m_col
                m_Index = CType(m_ht.Item(sIndex), Integer)
                ' eliminarlo de m_col
                m_col.RemoveAt(m_Index)
                '
                ' eliminarlo del hashtable
                m_ht.Remove(sIndex)
                '
                ' reasignar en m_ht el índice dentro de m_col
                ' a partir del elemento que se acaba de eliminar
                Dim i As Integer
                Dim sID As String
                For i = m_Index To m_col.Count - 1
                    ' Asignar en m_ht el nuevo índice dentro de m_col
                    sID = CType(m_col.Item(i), cPalabra).ID
                    m_ht.Item(sID) = i
                Next
                '
            End If
        End Sub

        Default Public Overridable ReadOnly Property Item(ByVal sIndex As String) As cPalabra
            Get
                ' Método predeterminado
                Dim tPalabra As cPalabra

                If m_ht.ContainsKey(sIndex) Then
                    m_Index = CType(m_ht.Item(sIndex), Integer)
                    Return CType(m_col.Item(m_Index), cPalabra)
                Else
                    ' Creamos una nuevo objeto
                    tPalabra = New cPalabra()
                    tPalabra.ID = sIndex
                    ' Incrementamos el contador de elementos
                    Contador += 1
                    ' lo añadimos a la colección
                    m_Index = m_col.Add(tPalabra)
                    m_ht.Add(sIndex, m_Index)
                    Return tPalabra
                    ' Eliminamos el objeto
                    tPalabra = Nothing
                End If
            End Get
        End Property

        Public Overridable Function Count() As Integer
            ' Método Count de las colección
            Count = m_col.Count()
        End Function

        Public Overridable Sub Add(ByVal tPalabra As cPalabra)
            ' Añadir un nuevo elemento a la colección
            '
            If m_ht.ContainsKey(tPalabra.ID) = False Then
                m_Index = m_col.Add(tPalabra)
                m_ht.Add(tPalabra.ID, m_Index)
                ' incrementamos el contador de elementos
                Contador += 1
            End If
        End Sub

        Public Sub New()
            MyBase.New()
            m_col = New ArrayList()
            m_ht = New Hashtable()
        End Sub

        Protected Overrides Sub Finalize()
            ' Destruimos las colecciones
            m_ht = Nothing
            m_col = Nothing
            MyBase.Finalize()
        End Sub

        Public Overridable Function Clone() As cPalabras
            ' Hacer una copia de este objeto                                (06/Oct/00)
            '
            ' Esta copia no se puede añadir a una colección que previamente contenga este objeto
            Dim tPalabras As cPalabras
            Dim tPalabra As cPalabra

            tPalabras = New cPalabras()

            ' Añadir a la nueva colección los elementos de la contenida en este objeto
            For Each tPalabra In m_col
                tPalabras.Add(tPalabra.Clone())
            Next tPalabra

            Clone = tPalabras
        End Function

        Public Overridable Sub Nuevo(ByVal unPalabra As cPalabra)
            ' Añadir un nuevo Palabra                                       (17/Nov/00)
            Dim sID As String
            Dim tPalabra As New cPalabra()
            ' Incrementamos el contador de elementos
            ' Este valor puede que no coincida con el número de elementos actuales
            Contador += 1
            ' Comprobar si ya existe...
            If m_ht.ContainsKey(tPalabra.ID) Then
                ' existe un elemento con ese ID
                ' por tanto, crear un nuevo ID
                sID = "M" & FormatNumber(Contador, , Microsoft.VisualBasic.TriState.True)
            Else
                sID = tPalabra.ID
            End If
            tPalabra = unPalabra.Clone(sID)
            m_Index = m_col.Add(tPalabra)
            m_ht.Add(sID, m_Index)
        End Sub
    End Class
End Namespace

Volver arriba


' La clase de buscar palabras en ficheros usando threads

'------------------------------------------------------------------------------
' Clase para buscar palabras en ficheros                            (25/Mar/01)
' usando threads
'
' ©Guillermo 'guille' Som, 2001
'------------------------------------------------------------------------------
Option Strict On

Imports System.IO
Imports System.Threading
'
Namespace Guille.Clases
    Public Class cBuscarPalabrasEnFicheros
        ' La colección de palabras a manipular
        Private lasPalabras As Guille.Clases.cPalabras
        '
        ' Necesitamos que este objeto produzca eventos
        Private mProcesarFic() As Guille.Clases.cProcesarFicheroThread
        Private WithEvents mProcesarFic2 As Guille.Clases.cProcesarFicheroThread
        ' Array para cada uno de los threads
        Private mThreadFic() As Thread
        ' Variable para controlar el número de Threads creados
        Private nThread As Integer = 0
        '
        ' Los eventos que producirá esta clase
        Public Event ProcesandoFichero(ByVal sMsg As String)
        Public Event FicherosProcesados(ByVal sMsg As String)
        '
        ' El constructor de la clase necesita la colección de palabras
        ' que se van a manipular por esta clase.
        Public Sub New(ByVal quePalabras As Guille.Clases.cPalabras)
            MyBase.New()
            '
            lasPalabras = quePalabras
            mProcesarFic2 = New Guille.Clases.cProcesarFicheroThread(lasPalabras)
        End Sub
        '
        Private Sub mProcesarFic2_ProcesandoFichero(ByVal sMsg As String)
            ' Este evento se produce cuando se está procesando un fichero
            ' por uno de los threads.
            ' Desde aquí producimos nuestro evento a la aplicación
            RaiseEvent ProcesandoFichero(sMsg)
        End Sub
        '
        Private Sub ProcesarSubDir(ByVal sDir As String, _
                                   Optional ByVal sExt As String = "*.*")
            '------------------------------------------------------------------
            ' Este procedimiento será llamado de forma recursiva para procesar
            ' cada uno de los directorios.
            '------------------------------------------------------------------
            Dim tDir As Directory
            Dim tSubDirs() As Directory
            '
            ' Asigna a tSubDirs los subdirectorios incluidos en sDir
            tSubDirs = tDir.GetDirectoriesInDirectory(sDir)
            ' Crear un thread para cada directorio a procesar
            nThread += 1
            ReDim Preserve mThreadFic(nThread)
            ReDim Preserve mProcesarFic(nThread)
            mProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras)
            mProcesarFic(nThread - 1).sDir = sDir
            mProcesarFic(nThread - 1).sExt = sExt
            ' Los procedimientos a usar no deben aceptar parámetros
            mThreadFic(nThread - 1) = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir))
            '
            ' Iniciar el thread
            mThreadFic(nThread - 1).Start()
            ' Llamada recursiva a este procedimiento para procesar los subdirectorios
            ' de cada directorio
            For Each tDir In tSubDirs
                ' Procesar todos los directorios de cada subdirectorio
                ProcesarSubDir(tDir.FullName, sExt)
            Next
        End Sub
        '
        Public Function Procesar(ByVal sDir As String, _
                                Optional ByVal sExt As String = "*.*", _
                                Optional ByVal conSubDir As Boolean = False _
                                ) As String
            '------------------------------------------------------------------
            ' Procesar los directorios del path indicado en sDir,
            ' buscando ficheros con la extensión sExt,
            ' y si se deben procesar los subdirectorios.
            '------------------------------------------------------------------
            '
            Dim totFiles As Integer = 0
            Dim s As String
            '
            ' Comprobar si se van a procesar los subdirectorios
            If conSubDir Then
                ProcesarSubDir(sDir, sExt)
            Else
                nThread += 1
                ReDim Preserve mThreadFic(nThread)
                ReDim Preserve mProcesarFic(nThread)
                mProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras)
                mProcesarFic(nThread - 1).sDir = sDir
                mProcesarFic(nThread - 1).sExt = sExt
                mThreadFic(nThread - 1) = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir))
                '
                mThreadFic(nThread - 1).Start()
            End If
            '
            ' Aquí llegará incluso antes de terminar todos los threads
            'Debug.WriteLine("Fin de Procesar todos los ficheros")
            '
            ' Comprobar si han terminado los threads
            Dim i, j As Integer
            Do
                j = 0
                For i = 0 To nThread - 1
                    ' Comprobar si alguno de los Threads está "vivo"
                    ' si es así, indicarlo para que continue el bucle
                    If mThreadFic(i).IsAlive() Then
                        j = 1
                    End If
                Next
                ' Esto es necesario, para que todo siga funcionando
                System.WinForms.Application.DoEvents()
            Loop While j = 1
            '
            'Debug.WriteLine("Han finalizado los threads")
            '
            ' Ahora podemos asignar el número de ficheros procesados
            totFiles = mProcesarFic2.TotalFicheros
            If totFiles = 1 Then
                s = " Procesado 1 fichero."
            Else
                s = " Procesados " & CStr(totFiles) & " ficheros."
            End If
            RaiseEvent FicherosProcesados(s)
            '
            ' Asignamos a cero el valor de total ficheros
            ' por si se vuelve a usar, para que no siga acumulando.
            mProcesarFic2.TotalFicheros = 0
            '
            Return s
        End Function
        '
    End Class
End Namespace



'------------------------------------------------------------------------------
' Clase para procesar cada fichero en un Thread diferente           (25/Mar/01)
'
' ©Guillermo 'guille' Som, 2001
'------------------------------------------------------------------------------
Option Strict On

Imports System.IO

Namespace Guille.Clases
    Public Class cProcesarFicheroThread
        ' La colección de palabras a manipular
        Private Shared lasPalabras As Guille.Clases.cPalabras
        '
        Public Shared Event ProcesandoFichero(ByVal sMsg As String)
        '
        Public sDir As String
        Public sExt As String
        '
        Public Shared TotalFicheros As Integer = 0
        '
        Public Sub New(ByVal quePalabras As Guille.Clases.cPalabras)
            MyBase.New()
            '
            lasPalabras = quePalabras
        End Sub
        '
        Public Sub ProcesarDir()
            '------------------------------------------------------------------
            ' Procesar los ficheros del path y extensión indicadas.
            ' Convertido en Sub para usar con Thread                (25/Mar/01)
            '
            ' Modificado para usar .Read y asignar un array Char    (24/Mar/01)
            '------------------------------------------------------------------
            '
            Dim tDir As Directory
            Dim tFiles() As File
            Dim i, n As Integer
            Dim s() As Char
            Dim sFile As IO.StreamReader
            '
            ' Asigna a tFiles un array con los nombres de los ficheros,
            ' path incluido
            tFiles = tDir.GetFilesInDirectory(sDir, sExt)
            '
            ' Examinar el contenido de cada fichero y trocearlo en palabras
            '
            n = tFiles.Length - 1
            For i = 0 To n
                RaiseEvent ProcesandoFichero(" Procesando: " & CStr(i + 1) & " / " & CStr(n + 1) & " " & tFiles(i).Name & " ...")
                '
                sFile = tFiles(i).OpenText()
                With sFile
                    ' Esto funcionará sólo si NO se ha guardado con formato UTF8
                    .SwitchEncoding(System.Text.Encoding.Default)
                    ReDim s(tFiles(i).Length.ToInt32 + 1)
                    .Read(s, 1, tFiles(i).Length.ToInt32)
                    .Close()
                End With
                '
                ' Buscar cada una de las palabras del fichero
                Desglosar(s)
            Next
            '
            TotalFicheros += tFiles.Length
        End Sub
        '
        Private Sub Desglosar(ByVal sText As Char())
            ' Desglosar en palabras individuales el contenido del array de bytes
            Dim i, k, n As Integer
            Dim tPalabra As Guille.Clases.cPalabra
            ' Estos signos se considerarán separadores de palabras
            Dim sSep As String = ".,;: ()<>[]{}¿?¡!/\'=+*-%&$|" & CChar(34) & CChar(9) & CChar(10) & CChar(13)
            Dim s As String
            Dim c As Char
            '
            s = ""
            n = sText.Length - 1
            For i = 1 To n
                c = sText(i)
                k = InStr(sSep, CStr(c))
                ' Si no es un separador y es la última letra        (24/Mar/01)
                If (k = 0 And i = n) Then
                    s &= c
                    ' Para que se procese la palabra
                    k = 1
                End If
                ' Si hay un separador o es el último caracter
                If (k > 0) Then
                    If s.Length > 0 Then
                        tPalabra = New Guille.Clases.cPalabra()
                        tPalabra.ID = s
                        tPalabra.Veces = 1
                        If lasPalabras.Exists(s) = False Then
                            lasPalabras.Add(tPalabra)
                        Else
                            ' Incrementar el número de esta palabra
                            lasPalabras(s).IncVeces()
                        End If
                    End If
                    s = ""
                Else
                    s &= c
                End If
            Next
        End Sub
    End Class
End Namespace

Volver arriba


Código alternativo usando una misma clase para usar Threads:

Este código une las dos clases que procesan los ficheros en una sola. El motivo de mostrar las dos formas, es para que sepas que se puede hacer, y lo más importante: cómo hacerlo.

' cBuscarPalabrasEnFicheros2.vb
'------------------------------------------------------------------------------
' Clase para buscar palabras en ficheros usando threads             (25/Mar/01)
'
' Revisado para llamar a un procedimiento de la misma clase         (27/Mar/01)
'
' ©Guillermo 'guille' Som, 2001
'------------------------------------------------------------------------------
Option Strict On

Imports System.IO
Imports System.Threading
'
Namespace Guille.Clases
    '
    Public Class cBuscarPalabrasEnFicheros
        ' Al declarar las variables y procedimientos como Shared,
        ' serán compartidas por todas las instancias de la clase.
        '
        ' La colección de palabras a manipular
        Private Shared lasPalabras As Guille.Clases.cPalabras
        '
        ' Los eventos que producirá esta clase
        Public Event FicherosProcesados(ByVal sMsg As String)
        Public Shared Event ProcesandoFichero(ByVal sMsg As String)
        '
        Public sDir As String
        Public sExt As String
        '
        Public Shared TotalFicheros As Integer = 0
        '
        ' Array para usar con los Threads
        Private mProcesarFic() As cBuscarPalabrasEnFicheros
        ' Array para cada uno de los threads
        Private mThreadFic() As Thread
        ' Variable para controlar el número de Threads creados
        Private nThread As Integer = 0
        '
        ' El constructor de la clase necesita la colección de palabras
        ' que se van a manipular por esta clase.
        Public Sub New(ByVal quePalabras As Guille.Clases.cPalabras)
            MyBase.New()
            '
            lasPalabras = quePalabras
        End Sub
        '
        Public Sub ProcesarDir()
            '------------------------------------------------------------------
            ' Procesar los ficheros del path y extensión indicadas.
            ' Convertido en Sub para usar con Thread                (25/Mar/01)
            '
            ' Modificado para usar .Read y asignar un array Char    (24/Mar/01)
            '------------------------------------------------------------------
            '
            Dim tDir As Directory
            Dim tFiles() As File
            Dim i, n As Integer
            Dim s() As Char
            Dim sFile As IO.StreamReader
            '
            ' Asigna a tFiles un array con los nombres de los ficheros,
            ' path incluido
            tFiles = tDir.GetFilesInDirectory(sDir, sExt)
            '
            ' Examinar el contenido de cada fichero y trocearlo en palabras
            '
            n = tFiles.Length - 1
            For i = 0 To n
                RaiseEvent ProcesandoFichero(" Procesando: " & CStr(i + 1) & " / " & CStr(n + 1) & " " & tFiles(i).Name & " ...")
                '
                sFile = tFiles(i).OpenText()
                With sFile
                    ' Esto funcionará sólo si NO se ha guardado con formato UTF8
                    .SwitchEncoding(System.Text.Encoding.Default)
                    ReDim s(tFiles(i).Length.ToInt32 + 1)
                    .Read(s, 1, tFiles(i).Length.ToInt32)
                    .Close()
                End With
                '
                ' Buscar cada una de las palabras del fichero
                Desglosar(s)
            Next
            '
            TotalFicheros += tFiles.Length
        End Sub
        '
        Private Sub Desglosar(ByVal sText As Char())
            ' Desglosar en palabras individuales el contenido del array de bytes
            Dim i, k, n As Integer
            Dim tPalabra As Guille.Clases.cPalabra
            ' Estos signos se considerarán separadores de palabras
            Dim sSep As String = ".,;: ()<>[]{}¿?¡!/\'=+*-%&$|" & CChar(34) & CChar(9) & CChar(10) & CChar(13)
            Dim s As String
            Dim c As Char
            '
            s = ""
            n = sText.Length - 1
            For i = 1 To n
                c = sText(i)
                k = InStr(sSep, CStr(c))
                ' Si no es un separador y es la última letra        (24/Mar/01)
                If (k = 0 And i = n) Then
                    s &= c
                    ' Para que se procese la palabra
                    k = 1
                End If
                ' Si hay un separador o es el último caracter
                If (k > 0) Then
                    If s.Length > 0 Then
                        tPalabra = New Guille.Clases.cPalabra()
                        tPalabra.ID = s
                        tPalabra.Veces = 1
                        If lasPalabras.Exists(s) = False Then
                            lasPalabras.Add(tPalabra)
                        Else
                            ' Incrementar el número de esta palabra
                            lasPalabras(s).IncVeces()
                        End If
                    End If
                    s = ""
                Else
                    s &= c
                End If
            Next
        End Sub
        '
        Private Sub ProcesarSubDir(ByVal sDir As String, _
                                   Optional ByVal sExt As String = "*.*")
            '------------------------------------------------------------------
            ' Este procedimiento será llamado de forma recursiva para procesar
            ' cada uno de los directorios.
            '------------------------------------------------------------------
            Dim tDir As Directory
            Dim tSubDirs() As Directory
            '
            ' Asigna a tSubDirs los subdirectorios incluidos en sDir
            tSubDirs = tDir.GetDirectoriesInDirectory(sDir)
            '
            ' Crear un thread para cada directorio a procesar
            nThread += 1
            ReDim Preserve mThreadFic(nThread)
            ReDim Preserve mProcesarFic(nThread)
            mProcesarFic(nThread - 1) = New cBuscarPalabrasEnFicheros(lasPalabras)
            mProcesarFic(nThread - 1).sDir = sDir
            mProcesarFic(nThread - 1).sExt = sExt
            ' Los procedimientos a usar no deben aceptar parámetros
            mThreadFic(nThread - 1) = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir))
            '
            ' Iniciar el thread
            mThreadFic(nThread - 1).Start()
            ' Llamada recursiva a este procedimiento para procesar los subdirectorios
            ' de cada directorio
            For Each tDir In tSubDirs
                ' Procesar todos los directorios de cada subdirectorio
                ProcesarSubDir(tDir.FullName, sExt)
            Next
        End Sub
        '
        Public Function Procesar(ByVal sDir As String, _
                                Optional ByVal sExt As String = "*.*", _
                                Optional ByVal conSubDir As Boolean = False _
                                ) As String
            '------------------------------------------------------------------
            ' Procesar los directorios del path indicado en sDir,
            ' buscando ficheros con la extensión sExt,
            ' y si se deben procesar los subdirectorios.
            '------------------------------------------------------------------
            '
            Dim totFiles As Integer = 0
            Dim s As String
            '
            ' Comprobar si se van a procesar los subdirectorios
            If conSubDir Then
                ProcesarSubDir(sDir, sExt)
            Else
                nThread += 1
                ReDim Preserve mThreadFic(nThread)
                ReDim Preserve mProcesarFic(nThread)
                mProcesarFic(nThread - 1) = New cBuscarPalabrasEnFicheros(lasPalabras)
                mProcesarFic(nThread - 1).sDir = sDir
                mProcesarFic(nThread - 1).sExt = sExt
                mThreadFic(nThread - 1) = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir))
                '
                mThreadFic(nThread - 1).Start()
            End If
            '
            ' Aquí llegará incluso antes de terminar todos los threads
            'Debug.WriteLine("Fin de Procesar todos los ficheros")
            '
            ' Comprobar si han terminado los threads
            Dim i, j As Integer
            Do
                j = 0
                For i = 0 To nThread - 1
                    ' Comprobar si alguno de los Threads está "vivo"
                    ' si es así, indicarlo para que continue el bucle
                    If mThreadFic(i).IsAlive() Then
                        j = 1
                    End If
                Next
                ' Esto es necesario, para que todo siga funcionando
                System.WinForms.Application.DoEvents()
            Loop While j = 1
            '
            'Debug.WriteLine("Han finalizado los threads")
            '
            ' Ahora podemos asignar el número de ficheros procesados
            totFiles = Me.TotalFicheros
            If totFiles = 1 Then
                s = " Procesado 1 fichero."
            Else
                s = " Procesados " & CStr(totFiles) & " ficheros."
            End If
            RaiseEvent FicherosProcesados(s)
            '
            ' Asignamos a cero el valor de total ficheros
            ' por si se vuelve a usar, para que no siga acumulando.
            Me.TotalFicheros = 0
            '
            Return s
        End Function
        '
    End Class
End Namespace

Volver arriba


Segunda alternativa: un Thread para cada fichero procesado:

Para terminar, te muestro el código para crear un Thread para cada fichero en lugar de para cada directorio.


'
'------------------------------------------------------------------------------
' Clase para buscar palabras en ficheros usando threads             (25/Mar/01)
'
' Revisado para llamar a un procedimiento de la misma clase         (27/Mar/01)
' Se crea un thread para cada fichero, no para cada directorio      (27/Mar/01)
'
' ©Guillermo 'guille' Som, 2001
'------------------------------------------------------------------------------
Option Strict On
Imports System.IO
Imports System.Threading
'
Namespace Guille.Clases
    '
    Public Class cBuscarPalabrasEnFicheros
        ' Al declarar las variables y procedimientos como Shared,
        ' serán compartidas por todas las instancias de la clase.
        '
        ' La colección de palabras a manipular
        Private Shared lasPalabras As Guille.Clases.cPalabras
        '
        ' Los eventos que producirá esta clase
        Public Event FicherosProcesados(ByVal sMsg As String)
        Public Shared Event ProcesandoFichero(ByVal sMsg As String)
        '
        Public sDir As String
        Public sExt As String
        '
        Public TotalFicheros As Integer = 0
        '
        Public unFile As File
        '
        ' Arrays para usar con los Threads
        ' Para usar con los directorios
        Private mThreadDir() As Thread
        Private mProcesarDir() As cBuscarPalabrasEnFicheros
        Private nThreadsDir As Integer = 0
        '
        ' Para usar con los ficheros
        Private mThreadFic() As Thread
        Private mProcesarFic() As cBuscarPalabrasEnFicheros
        Private nThreadsFic As Integer = 0
        '
        '
        ' El constructor de la clase necesita la colección de palabras
        ' que se van a manipular por esta clase.
        Public Sub New(ByVal quePalabras As Guille.Clases.cPalabras)
            MyBase.New()
            '
            lasPalabras = quePalabras
        End Sub
        '
        Private Sub UnFichero()
            Dim s() As Char
            Dim sFile As IO.StreamReader
            '
            sFile = unFile.OpenText()
            With sFile
                ' Esto funcionará sólo si NO se ha guardado con formato UTF8
                .SwitchEncoding(System.Text.Encoding.Default)
                ReDim s(unFile.Length.ToInt32 + 1)
                .Read(s, 1, unFile.Length.ToInt32)
                .Close()
            End With
            '
            ' Buscar cada una de las palabras del fichero
            Desglosar(s)
        End Sub
        '
        Private Sub ProcesarDir()
            '------------------------------------------------------------------
            ' Procesar los ficheros del path y extensión indicadas.
            ' Convertido en Sub para usar con Thread                (25/Mar/01)
            '
            ' Modificado para usar .Read y asignar un array Char    (24/Mar/01)
            '------------------------------------------------------------------
            '
            Dim tDir As Directory
            Dim tFiles() As File
            Dim i, n As Integer
            Dim s() As Char
            Dim sFile As IO.StreamReader
            '
            ' Asigna a tFiles un array con los nombres de los ficheros,
            ' path incluido
            tFiles = tDir.GetFilesInDirectory(sDir, sExt)
            '
            'Debug.WriteLine(sDir)
            '
            ' Examinar el contenido de cada fichero y trocearlo en palabras
            '
            n = tFiles.Length - 1
            'Debug.WriteLine(sDir & "= " & CStr(tFiles.Length - 1) & " " & CStr(Ubound(tFiles)))
            '
            For i = 0 To n
                RaiseEvent ProcesandoFichero(" Procesando: " & CStr(i + 1) & " / " & CStr(n + 1) & " " & tFiles(i).Name & " ...")
                '
                ' Crear un thread para cada fichero a procesar
                nThreadsFic += 1
                ReDim Preserve mThreadFic(nThreadsFic)
                ReDim Preserve mProcesarFic(nThreadsFic)
                mProcesarFic(nThreadsFic - 1) = New cBuscarPalabrasEnFicheros(lasPalabras)
                mProcesarFic(nThreadsFic - 1).unFile = tFiles(i)
                ' Asignación del procedimiento a usar por el Thread
                mThreadFic(nThreadsFic - 1) = New Thread(New ThreadStart(AddressOf mProcesarFic(nThreadsFic - 1).UnFichero))
                '
                ' Iniciar el thread
                mThreadFic(nThreadsFic - 1).Start()
                '
            Next
            '
            TotalFicheros += tFiles.Length
        End Sub
        '
        Private Sub Desglosar(ByVal sText As Char())
            ' Desglosar en palabras individuales el contenido del array de bytes
            Dim i, k, n As Integer
            Dim tPalabra As Guille.Clases.cPalabra
            ' Estos signos se considerarán separadores de palabras
            Dim sSep As String = ".,;: ()<>[]{}¿?¡!/\'=+*-%&$|" & CChar(34) & CChar(9) & CChar(10) & CChar(13)
            Dim s As String = ""
            Dim c As Char
            '
            n = sText.Length - 1
            For i = 1 To n
                c = sText(i)
                k = InStr(sSep, CStr(c))
                ' Si no es un separador y es la última letra        (24/Mar/01)
                If (k = 0 And i = n) Then
                    s &= c
                    ' Para que se procese la palabra
                    k = 1
                End If
                ' Si hay un separador o es el último caracter
                If (k > 0) Then
                    If s.Length > 0 Then
                        tPalabra = New Guille.Clases.cPalabra()
                        tPalabra.ID = s
                        tPalabra.Veces = 1
                        If lasPalabras.Exists(s) = False Then
                            lasPalabras.Add(tPalabra)
                        Else
                            ' Incrementar el número de esta palabra
                            lasPalabras(s).IncVeces()
                        End If
                    End If
                    s = ""
                Else
                    s &= c
                End If
            Next
        End Sub
        '
        Private Sub ProcesarSubDir(ByVal sDir As String, _
                                   Optional ByVal sExt As String = "*.*")
            '------------------------------------------------------------------
            ' Este procedimiento será llamado de forma recursiva para procesar
            ' cada uno de los directorios.
            '------------------------------------------------------------------
            Dim tDir As Directory
            Dim tSubDirs() As Directory
            '
            ' Asigna a tSubDirs los subdirectorios incluidos en sDir
            tSubDirs = tDir.GetDirectoriesInDirectory(sDir)
            '
            'Debug.WriteLine(sDir)
            '
            Me.sDir = sDir
            Me.sExt = sExt
            ProcesarDir()
            '
            ' Llamada recursiva a este procedimiento para procesar los subdirectorios
            ' de cada directorio
            For Each tDir In tSubDirs
                ' Procesar todos los directorios de cada subdirectorio
                ProcesarSubDir(tDir.FullName, sExt)
            Next
        End Sub
        '
        Public Function Procesar(ByVal sDir As String, _
                                Optional ByVal sExt As String = "*.*", _
                                Optional ByVal conSubDir As Boolean = False _
                                ) As String
            '------------------------------------------------------------------
            ' Procesar los directorios del path indicado en sDir,
            ' buscando ficheros con la extensión sExt,
            ' y si se deben procesar los subdirectorios.
            '------------------------------------------------------------------
            '
            Dim totFiles As Integer = 0
            Dim s As String
            '
            ' Comprobar si se van a procesar los subdirectorios
            If conSubDir Then
                ProcesarSubDir(sDir, sExt)
            Else
                '
                Me.sDir = sDir
                Me.sExt = sExt
                ProcesarDir()
                '
            End If
            '
            ' Aquí llegará incluso antes de terminar todos los threads
            'Debug.WriteLine("Fin de Procesar todos los ficheros")
            '
            ' Comprobar si han terminado los threads
            Dim i, j As Integer
            '
            ' Esperar a que terminen los Threads de cada fichero
            Do
                j = 0
                For i = 0 To nThreadsFic - 1
                    ' Comprobar si alguno de los Threads está "vivo"
                    ' si es así, indicarlo para que continue el bucle
                    If mThreadFic(i).IsAlive() Then
                        j = 1
                    End If
                Next
                ' Esto es necesario, para que todo siga funcionando
                System.WinForms.Application.DoEvents()
            Loop While j = 1
            '
            ' Eliminar los arrays de la memoria
            nThreadsFic = 0
            Erase mThreadFic
            Erase mProcesarFic
            '
            'Debug.WriteLine("Han finalizado los threads de cada fichero")
            '
            ' Ahora podemos asignar el número de ficheros procesados
            totFiles = Me.TotalFicheros
            If totFiles = 1 Then
                s = " Procesado 1 fichero."
            Else
                s = " Procesados " & CStr(totFiles) & " ficheros."
            End If
            RaiseEvent FicherosProcesados(s)
            '
            ' Asignamos a cero el valor de total ficheros
            ' por si se vuelve a usar, para que no siga acumulando.
            Me.TotalFicheros = 0
            '
            Return s
        End Function
        '
    End Class
End Namespace

Volver arriba


Ir al índice de vb.net

la Luna del Guille o... el Guille que está en la Luna... tanto monta...