Sigue este tutorial para aprender como crear palabras activas (en las que puedes hacer clic) en el texto de un control TextArea utilizando el patrón de diseño Observer, el cual te permite cambiar de forma dinámica el modo en el que reaccionará la aplicación cuando el usuario haga clic sobre cualquiera de estas palabras activas. Lo mejor de todo es que es multiplataforma, de modo que puedes usarlo en macOS, Windows y Linux.
Nuestro ejemplo de palabras activas está basado en el uso del tipo de dato Pair, de modo que uno de los fragmentos de información será la palabra (o palabras) que queramos detectar, y el otro fragmento de información del Pair será cualquier dato asociado que desees… siempre que sea un String. Por ejemplo, el dato asociado podría ser un enlace o un texto que quieras mostrar en otro control cuando el usuario haga clic en la palabra.
Para mantener este tutorial lo más breve posible, la clase creada tiene algunas limitaciones. Por ejemplo, no permite repetir la misma palabra (o combinación de palabras) asignando a cada una de ellas una acción distinta. Si lo haces, la clase siempre reaccionará a la primera correspondencia encontrada. Si quieres que tu código reaccione del mismo modo a todas las ocurrencias de una misma palabra… entonces está bien. En cualquier caso, este tutorial sirve como un buen punto de partida para que puedas modificarlo y mejorarlo para que se ajuste a tus necesidades.
Puedes descargar la clase con el proyecto de ejemplo desde este enlace.
Subclase TextArea
Esta característica estará basada en la clase TextArea, de modo que el primer paso es añadir un control TextArea desde el panel Librería al Navegador, cambiando su nombre a LinkDetectorTextArea
. A continuación, añade los siguientes manejadores de eventos a la subclase TextArea. Estos serán los responsables de seguir el movimiento del apuntador y reaccionar ante cualquiera de las palabras activas que se hayan registrado para la instancia:
- Open. Aquí asignaremos el cursor de tipo Pointer, más apropiado para el tipo de funcionalidad que proporcionará esta subclase.
- MouseMove. Aquí es donde detectaremos si se encuentra una palabra activa bajo la posición del apuntador.
- MouseDown. Este es el manejador de evento que invocará a los Observadores registrados cuando el usuario haga clic en una palabra activa, pasando la palabra activa junto con la información asociada.
Dado que queremos que estos eventos estén disponibles también para cualquier instancia creada a partir de nuestra subclase, deberemos de añadir de nuevo las definiciones de eventos que hemos consumido. Con la subclase seleccionada en el Navegador, selecciona Insert > Event Definition
utilizando las siguientes signaturas:
- Event Name. Open.
- Event Name. MouseMove. Parameters:
X as Integer, Y as Integer
. - Event Name. MouseDown. Parameters:
X as Integer, Y as Integer
. Return Type:Boolean
.
A continuación tendremos que añadir a la clase algunas propiedades Privadas y que serán usadas por los manejadores de eventos y otros métodos que aún tenemos que definir. Estas propiedades tendrán un ámbito Privado dado que sólo es necesario acceder a ellas desde la clase propiamente dicha y no desde otros fragmentos de código externo o clases adicionales heredadas a partir de nuestra subclase:
- Name. boundedWord. Type:
Pair
. Esta es la propiedad que apuntará a la palabra activa actual del texto. - Name. previousBoundedWord. Type:
Pair
. Esta propiedad apunta a la palabra activa que se haya encontrado con anterioridad en el texto. - Name. linkedWords. Type:
Dictionary
. Este es el Diccionario que contiene todas los pares de palabras activas junto con su información asociada. - Name. observers(). Type:
ActionDelegate
. Un Array que contendrá todos los observadores registrados, con el tipoActionDelegate
aún por definir.
Como hemos visto, hemos de definir un tipo de dato Delegado en nuestra clase. Selecciona Insert > Delegate
utilizando la siguiente información:
- Delegate Name: ActionDelegate.
- Parameters: boundedWord as Pair.
- Scope: Public.
Asignar el tipo de cursor
Selecciona el evento Open
y añade el siguiente código en el Editor de Código asociado. Utilizaremos este evento simplemente para asignar el cursor de flecha que es más apropiado para el tipo de funcionalidad ofrecido:
Me.MouseCursor = System.cursors.standardpointer RaiseEvent open
Detectar las Palabras Activas
Selecciona a continuación el manejador de evento MouseMove
y escribe el siguiente código en el Editor de Código asociado. Este código se encarga de detectar los límites de la palabra que se encuentra bajo la posición del cursor, además de averiguar si se trata de una de las palabras activas registradas para la instancia de la clase:
Var tlen As Integer = Me.Text.Length If previousBoundedWord <> Nil Then setStyleForWord(previousBoundedWord, False) previousBoundedWord = Nil End If Var boundLeft, boundRight As Integer = -1 Var startPosition As Integer = Me.CharacterPosition(x,y) If CharacterPosition(x,y) >= tLen Then If boundedWord <> Nil Then setStyleForWord( boundedWord, False ) mboundedWord = Nil ElseIf Me.Text.Middle(startPosition,1) <> Chr(32) And Me.Text.Middle(startPosition,1) <> EndOfLine Then For n As Integer = startPosition DownTo 0 If Me.Text.Middle(n,1) = Chr(32) Or Me.Text.Middle(n,1) = EndOfLine or n = 0 Then boundLeft = n Exit End Next For n As Integer = startPosition To tlen If Me.Text.Middle(n+1,1) = Chr(32) Or Me.Text.Middle(n+1,1) = EndOfLine Or n = tlen Then boundRight = n+1 Exit End Next End If boundLeft <> -1 And boundRight <> -1 Then Var isolatedWord As String = Me.Text.Middle(boundLeft, boundRight - boundLeft) Var check As pair = wordInDictionary( isolatedWord, boundleft, boundRight ) If check <> Nil Then mboundedWord = check If previousBoundedWord = Nil Then previousBoundedWord = mboundedWord setStyleForWord(previousBoundedWord, True) Else mboundedWord = Nil End If End If RaiseEvent mouseMove(X, Y)
Reaccionar a las Palabras Activas
Por último, selecciona el manejador de evento MouseDown
y escribe el siguiente código en el Editor de Código asociado. Este código invocará a todos los observadores registrados para la palabra activa bajo el apuntador (si es el caso):
If linkedWords <> Nil And boundedWord <> Nil Then Var p As New pair(mboundedWord.Left,linkedWords.Value(mboundedWord.Left)) For Each item As LinkDetectorTextArea.ActionDelegate In observers item.Invoke p Next Return True End If Return RaiseEvent mousedown(x,y)
Registrar las Palabras Activas
Necesitamos un modo de informar a las instancias sobre las palabras activas que ha de detectar. Podríamos hacerlo utilizando el Constructor o bien usando una Propiedad Calculada en vez de una propiedad normal. En este caso utilizaremos un método para asignar el Diccionario recibido de pares a la Propiedad a cargo de almacenar dicha información. Por tanto, selecciona Insert > Method
y utiliza los siguientes datos en el Panel Inspector asociado:
- Name: setDictionary.
- Parameters: d As Dictionary.
- Scope: Public.
En el Editor de Código asociado sólo hemos de incluir la siguiente línea de código:
linkedWords = d
Registrando (y borrando) los Observadores
Como hemos dicho, nuestra clase también podrá reaccionar a las palabras activas, de modo que necesitamos añadir un par de métodos para registrar y borrar los Observadores en cualquier momento. Utiliza la siguiente información para el primer método:
- Method Name: registerObserver.
- Parameters: observer as ActionDelegate.
- Scope: Public.
Y escribe esta línea de código en Editor de Código asociado:
if observer <> nil then observers.add observer
Añade un segundo método que estará a cargo de eliminar un observador. Utiliza los siguientes datos para la signatura:
- Method Name: deleteObserver.
- Parameters: observer As LinkDetectorTextArea.ActionDelegate.
- Scope: Public.
Y este es el código encargado de buscar y eliminar el ActionDelegate
recibido en el array de observadores de la instancia:
If observer <> Nil Then Var n As Integer = observers.IndexOf(observer) observers.RemoveAt(n) end
¡Expresiones Regulares y estilo al rescate!
Por último sólo necesitamos añadir un par de métodos más para finalizar nuestra subclase de TextArea. Estos serán métodos auxiliares Privados para la clase. El primero será el método a cargo de encontrar la palabra candidata recibida entre las registradas como activas y, si la encuentra, devolverá el correspondiente Par de datos. Para ello añade un nuevo método utilizando los siguientes datos de signatura en el Panel Inspector:
- Method Name: wordInDictionary.
- Parameters: word As string, leftposition As integer, RightPosition As integer.
- Return Type: Pair.
- Scope: Private.
Y escribe el siguiente código en el Editor de Código asociado:
#Pragma Unused RightPosition Var theRightPosition As Integer Var re As New RegEx re.SearchPattern = "[a-zA-Z]+" Var rm As RegExMatch rm = re.Search(word) If rm <> Nil Then Var foundword As String = rm.SubExpressionString(0) Var characterPosition As Integer = word.LeftBytes(rm.SubExpressionStartB(0)).Length word = foundword leftposition = leftposition + characterPosition therightposition = leftposition + word.Length End If If linkedWords.HasKey(word) Then Return New pair(word,leftposition.ToString+"-"+ theRightPosition.ToString) Dim blu As Integer dim dictionaryKeys() as variant = linkedWords.Keys for each thekey as string in dictionaryKeys If thekey.IndexOf(word) <> -1 Then blu = Me.Text.IndexOf(thekey) If leftposition >= blu And leftposition <= (blu + thekey.Length) Then Var finalposition As Integer =blu+thekey.Length Return New pair(thekey,blu.ToText+"-"+finalposition.ToString) end if end next Return nil
Añadir Estilo
Por último, necesitamos un modo de informar al usuario de que la palabra bajo el apuntador es una palabra activa, de modo que pueda hacer clic sobre ella. Para este ejemplo he utilizado el estilo de texto Subrayado, pero puedes adaptarlo a tus preferencias. Por tanto, añade el último método de la clase utilizando los siguientes datos:
- Method Name: setStyleForWord
- Parameters: word As pair, mode As Boolean.
- Scope: Private.
Utilizamos el parámetro mode
de modo que podamos usar el método tanto para aplicar como para eliminar el estilo en la palabra recibida. Así podremos eliminar el subrayado de la palabra activa que se hubiese detectado con anterioridad, aplicando dicho estilo sobre la nueva palabra detectada. Escribe el siguiente código en el Editor de Código asociado:
Var cStart, cEnd As Double cStart = word.Right.StringValue.NthField("-",1).Val cEnd = word.Right.StringValue.NthField("-",2).Val - cStart Var t As StyledText t = Me.StyledText cstart = If( cstart - 1 < 0, 0, cstart) t.Underline(cstart, cEnd) = mode
¡Poniéndolo en marcha!
Con nuestra subclase terminada es el momento de diseñar la ventana de la interfaz de usuario de modo que podamos probar la funcionalidad de la clase. Selecciona el ítem Window1
en el Navegador para acceder al Editor de Diseño de la ventana. A continuación, arrastra la subclase LinkDetectorTextArea
desde el Navegador sobre el Editor de Diseño para crear una nueva instancia. Utiliza el Panel Inspector para cambiar su nombre a myURLField
. Añade a continuación tres controles Label y un HTMLViewer
desde la Librería. EL HTMLViewer mostrará el URL asociado con la palabra activa, mientras que uno de los tres controles Label nos mostrará cuál ha sido la palabra activa detectada bajo el cursor. La tarea de cargar el URL y de mostrar la palabra detectada se llevará a cabo mediante un observador previamente registrado (un método en la ventana). El diseño de la ventana ya finalizado será similar al siguiente:
Donde el texto para la etiqueta “Sample Text” es el asignado a la propiedad Text
de la instancia LinkDetectorTextArea
.
Vamos a añadir ahora el método al objeto Window1
que actuará como observador. Selecciona la opción Add > Method
e introduce los siguientes datos en el Panel Inspector asociado:
- Name: updateContents
- Parameters: p as pair
- Scope: Public
Escribiendo a continuación el siguiente código en el Editor de Código asociado:
linkedControl.Text = p.Left HTMLViewer1.LoadURL p.Right
Por último, añade ahora el evento Open
a la ventana Window1
, e introduce el siguiente código en el Editor de Código asociado. Aquí es donde inicializaremos nuestra instancia LinkDetectorTextArea
proporcionándole las palabras activas junto con los URL asociados a cada una de ellas:
myURLField.setDictionary New Dictionary("Xojo":"https://www.xojo.com","iOS":"http://www.apple.com/ios/",_ "macOS":"http://www.apple.com/osx/", "Windows":"https://www.microsoft.com/en-US/windows","Linux":"https://es.wikipedia.org/wiki/GNU/Linux",_ "Raspberry Pi":"https://www.raspberrypi.org/","AprendeXojo":"https://www.aprendexojo.com","Web":"https://www.xojo.com/web") myURLField.registerObserver WeakAddressOf updateContents
Ejecuta el proyecto, mueve el apuntador sobre el texto y veras cómo se subrayan las palabras activas a medida que pases el apuntador sobre ellas; y si haces clic sobre cualquiera de las palabras activas, entonces la ventana mostrará tanto la palabra detectada como la página web correspondiente al URL asociado.
Por supuesto, puedes mejorar y ampliar en gran medida la funcionalidad de esta subclase para adaptarla más a tus necesidades.