This post was originally published in 2018 and has been updated to use Xojo API 2.0.
Follow this tutorial to create active, aka clickable, words in the text of a TextArea control in your Xojo projects. Learn to use the Object-oriented Delegate design pattern to dynamically change how your app reacts when the user clicks on those active words. Best of all, this project is cross-platform, so you can use it for macOS, Windows and Linux!
The active words will be based on the use of Pairs, so one piece of information will be the active word and the other piece of information will be the associated data, which is always a String. The associated data could be a link or text you want to show in another control when the user clicks on the active word.
For the sake of this tutorial, the class we are creating has some limitations. For example, you won’t be able to repeat the same word and assign it to take different actions. If you want your code to react the same to the same active word or words throughout your project, well then, that’s what it will do. Anyway, this tutorial serves as a good starting point and you’ll be able to modify and improve it to meet your own needs.
Download the class with the example project from here.
TextArea Subclass
This feature will be based in the TextArea class, the first step is add a new TextArea control from the Library panel to the Navigator, change its name to LinkDetectorTextArea
. Next, add the following Event Handlers to the TextArea subclass. These will be responsible for following the pointer movement and reacting to the registered active words for the instance.
- Open. To assign the pointer cursor, more appropriate for the kind of functionality this subclass will provide
- MouseMove. To detect if there is an active word under the cursor pointer to click on
- MouseDown. To call all the registered Observers when the user clicks on an active word, passing along the detected word and the associated information
Since we also want to make these events available for any instance created from the subclass, we have to create them again as Event Definitions. With the subclass selected in the Navigator, select Insert > Event Definition
using the following signatures:
- 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
.
Add some Private properties used by the added Event Handlers and other methods we have yet to define to the class. Set these properties as private because they just need to be accessed from the class itself and not from other pieces of external code or additional classes inherited from the subclass:
- Name. boundedWord. Type:
Pair
. This is the property that will point to the current matched active word from the text. - Name. previousBoundedWord. Type:
Pair
. This property points to the previous matched word from the text. - Name. linkedWords. Type:
Dictionary
. This is the Dictionary that will contain all the active words / additional data pairs. - Name. observers(). Type:
ActionDelegate
. This is an Array containing all the registered observers of the typeActionDelegate
yet to be defined.
As we have seen, we need to define a Delegate type for the class. Select Insert > Delegate
with the following data:
- Delegate Name: ActionDelegate.
- Parameters: boundedWord as Pair.
- Scope: Public.
Assigning the Cursor Type
Select the Open
Event and add the following code in the corresponding Code Editor:
Me.MouseCursor = System.cursors.standardpointer RaiseEvent open
We simply use this Event for assigning the Arrow cursor type that is most appropriate.
Matching the Active Words
Next, select the MouseMove
Event Handler and type the following code in the corresponding Code Editor:
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)
This code is in charge of detecting the limits of the word under the cursor and finding if it is one of the active words registered for the class.
Reacting to Active Words
Finally, select the MouseDown
Event Handler and write the following code into the associated Code Editor:
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)
This code will invoke all the registered Observers for the active word detected under the cursor.
Registering the Active Words
We need a way to inform the instances about the active words to be aware of. We could do that from a Constructor or using a Computed Property. In this case, we will use a method for assigning the received Dictionary of Pairs to the Property in charge of storing that information. Select Insert > Method
and use this data in the associated Inspector Panel:
- Name: setDictionary.
- Parameters: d As Dictionary.
- Scope: Public.
In the associated Code Editor, put this simple line of code:
linkedWords = d
Registering (and Deregistering) Observers
As we have said, the class will be able to react to the active words, so we need to provide a couple of methods to register and deregister Observers at any time. Use the following information for the first method:
- Method Name: registerObserver.
- Parameters: observer as ActionDelegate.
- Scope: Public.
And write this line of code in the associated Code Editor:
if observer <> nil then observers.add observer
Add a second method that will be in charge of deregistering an Observer. Use the following data for that:
- Method Name: deleteObserver.
- Parameters: observer As LinkDetectorTextArea.ActionDelegate.
- Scope: Public.
This is the code that will search and remove the received ActionDelegate
from the instance Array of Observers:
If observer <> Nil Then Var n As Integer = observers.IndexOf(observer) observers.RemoveAt(n) end
Regular Expressions to the Rescue … and Styling!
Finally, add a couple of methods to finish the TextArea subclass. These will be Private for the class, auxiliary methods called mainly from the Event Handlers. The first one is the method that does the matching with the received candidate and upon finding a match, will return the corresponding data pair to the caller. To do this, add a new Method using the following data:
- Method Name: wordInDictionary.
- Parameters: word As string, leftposition As integer, RightPosition As integer.
- Return Type: Pair.
- Scope: Private.
And write the following code in the associated Code Editor:
#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) Var blu As Integer Var 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
Adding Style
Finally, we need to choose how to inform the user that the word below the cursor is an active word so they can click on it. For this example, I’ve used the Underline text style, but you can change that. Add the last method for the class using the following data:
- Method Name: setStyleForWord
- Parameters: word As pair, mode As Boolean.
- Scope: Private.
Use the mode
parameter in order to use the method to apply or delete the style. For example, removing the style from the previous detected active word and styling the new one. Write the following code in the associated Code Editor:
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
Putting it all together!
With the subclass finished it is time to do some Window Layout so we can test the subclass functionality. Select the Window1
item in the Navigator to access the Window Layout Editor and drag the LinkDetectorTextArea
subclass from the Navigator onto the Window Layout. Use the Inspector to change its name to myURLField
. Then add three Label controls and an HTMLViewer, both from the Library. The HTMLViewer will show the URL associated with the active word, while one of the three Label controls will show the detected active words. Both the URL loading as the displaying of the detected word will be done by a previously registered Observer (a window method). The finished window layout should look something like this:
Where the text under the “Sample Text” label is the one assigned to the Text
property of the LinkDetectorTextArea
instance.
Now, let’s add to the Window1
object the method that will act as an Observer, select the Add > Method
option in combination with the following data:
- Name: updateContents
- Parameters: p as pair
- Scope: Public
And put the following code in the associated Code Editor:
linkedControl.Text = p.Left HTMLViewer1.LoadURL p.Right
Lastly, add the Open
event to the Window1
window, adding the following code to the associated Code Editor. This will initialize the LinkDetectorTextArea
instance providing the clickable words and their associated URLs:
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
Run the project, move the cursor over the text and you will see the how the active words are underlined on the fly; and if you click on any of them, the Window will display the detected word, while loading the associated URL.
Of course, this can be vastly improved and expanded upon … enjoy!
Paul learned to program in BASIC at age 13 and has programmed in more languages than he remembers, with Xojo being an obvious favorite. When not working on Xojo, you can find him talking about retrocomputing at Goto 10 and on Mastodon @lefebvre@hachyderm.io.