Tutorial: Palabras activas (API 2.0)

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 tipo ActionDelegate 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.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *