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.
- Crear y usar Threads, los conceptos básicos.
- Crear array de Threads.
- Procesar todos los subdirectorios de un directorio.
- Procesar todos los ficheros de un directorio.
- Acceder a ficheros: guardar y leer, (usando el formato estándar UTF8), con StreamWriter y StreamReader.
- Leer en un array del tipo Char el contenido de un fichero ANSI usando StreamReader.
- Crear una clase/colección para almacenar las palabras halladas.
- Usar ArrayList y HashTable para crear nuestras propias colecciones.
- Usar eventos en nuestras clases y en array de clases.
- Usar diálogos comunes para seleccionar un fichero leer y guardar.
- Añadir ToolTips a los controles de un formulario.
¡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.
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
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 NextLa 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.
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 - 1Al 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)
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.
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 ' ... NextEl método Read del objeto StreamReader asigna a un array del tipo Char los caracteres que le indiquemos: desde que posición y cuantos.
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.
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 FunctionEn 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...
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 SubAunque 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 SubEventos 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.
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 ThenCon 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.
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.
El código del proyecto de contar palabras usando Threads (WordCountThread):
Links a los distintos ficheros del código:
- El formulario
- La clase/colección para almacenar las palabras
- La clase llamada por cada Thread
- La clase que llama a cada Thread
- La clase alternativa (llamar a los Threads desde el mismo fichero)
- Segunda alternativa: un Thread para cada fichero procesado
El código completo del proyecto está en este fichero zip: (WordCountThread.zip 17.0KB)
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'------------------------------------------------------------------------------ ' 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' 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
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
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