Follow this tutorial to learn how to create active (clickable) words in a text of a TextArea control using the OOP Delegate design pattern, which allows you to dynamically change how your app will react when the user clicks on any of these active words. Best of all, this is cross-platform, so you can use it for macOS, Windows and Linux deployments!
Our active words will be based on the use of Pairs, so one of the pieces of information will be the active word (or words) we want to detect on the text, and the other piece of information for the Pair will be any associated data you want…this is always a String. For example, the associated data could be a link or text you want to show in another control when the user clicks on the word.
For the sake of this tutorial, the class created has some limitations. For example, you can’t repeat the same word (or same combination of words) assigning them to different actions. If you do that, the class will alway react to the first occurrence found; if you want your code to always react the same to the same active word or words…then that’s ok. Anyway, this tutorial serves as a good starting point so you can modify and improve it to meet your own needs.
You can download the class with the example project from here.
TextArea subclass
This feature will be based in the TextArea class, so the first step is add a new TextArea control from the Library panel to the Navigator, changing its name to LinkDetectorTextArea
. Next, add the following Event Handlers to our TextArea subclass. These will be responsible for following the pointer movement and reacting to the registered active words for the instance:
- Open. Just to assign the Pointer cursor, more appropriate for the kind of functionality this subclass will provide.
- MouseMove. Here is where we will detect if there is an active word (or combination of words) found under the cursor pointer.
- MouseDown. This is the Event Handler that will call all the registered Observers when the user clicks on an active word, passing along the detected word and the associated information.
As we want to make these events also available for any instance created from our subclass, then 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
.
Next we will need to add to the class some Private properties used by the added Event Handlers and other methods yet to define. These properties will be set 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 our own 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
. Array containing all the registered observers of the typeActionDelegate
yet to define.
As we have seen, we need to define a Delegate type for our 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. We simply use this Event for assigning the Arrow cursor type that is more appropriate for the kind of functionality offered:
Me.MouseCursor = System.cursors.standardpointer RaiseEvent open
Matching the Active Words
Next, select the MouseMove
Event Handler and type the following code in the corresponding Code Editor. 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:
Dim length As Integer = Me.Text.Len if previousBoundedWord <> nil then setStyleForWord(previousBoundedWord, false) previousBoundedWord = nil end if dim boundLeft, boundRight as integer Dim startPosition As Integer = Me.CharPosAtXY(x,y) If CharPosAtxy(x,y) >= length Then if boundedWord <> nil then setStyleForWord( boundedWord, false ) mboundedWord = nil elseif me.text.mid(startPosition,1) <> chr(32) and me.Text.mid(startPosition,1) <> EndOfLine then for n as integer = startPosition DownTo 1 if me.Text.mid(n-1,1) = chr(32) or me.Text.mid(n-1,1) = EndOfLine then boundLeft = n exit end next for n as integer = startPosition to length if me.Text.mid(n+1,1) = chr(32) or me.Text.mid(n+1,1) = EndOfLine or n = length then boundRight = n+1 exit end next end dim isolatedWord as string = me.Text.mid(boundLeft, boundRight - boundLeft) dim 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 RaiseEvent mouseMove(X, Y)
Reacting to Active Words
Finally, select the MouseDown
Event Handler and write the following code into the associated Code Editor. This code will invoke all the registered observers for the active word detected under the cursor pointer (if any):
If linkedWords <> Nil And boundedWord <> Nil Then Dim 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)
Registering the Active Words
We need a way to inform the instances about the active words it has to be aware of. We could do that from a Constructor or using a Computed Property instead of a regular one. In this case, we will use a method for assigning the received Dictionary of Pairs to the Property in charge of storing that information. So, 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 we just need to put this simple line of code:
linkedWords = d
Registering (and deregistering) Observers
As we have said, our 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.Append 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 observers.Remove( observers.IndexOf( observer ) ) end
Regular Expressions to the rescue… and Styling!
Finally we just need to add a couple of methods more for finishing our TextArea subclass. These will be Private for the class, auxiliary methods called mainly from the Event Handlers. The first one will be the method in charge of doing the matching from the received candidate, and if it finds it, then it will return the corresponding pair of data to the caller. For this one, 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:
Dim theRightPosition As Integer dim re as new RegEx re.SearchPattern = "[a-zA-Z]+" dim rm as RegExMatch rm = re.Search(word) if rm <> nil then dim foundword as string = rm.SubExpressionString(0) dim characterPosition as integer = word.LeftB(rm.SubExpressionStartB(0)).Len word = foundword leftposition = leftposition + characterPosition therightposition = leftposition + word.Len end if if linkedWords.HasKey(word) then Return new pair(word,leftposition.ToText+"-"+ theRightPosition.ToText) dim blu as integer dim dictionaryKeys() as variant = linkedWords.Keys for each thekey as string in dictionaryKeys if thekey.InStr(word) > 0 then blu = me.text.instr(thekey) if leftposition >= blu and leftposition <= (blu + thekey.Len) then dim finalposition as integer =blu+thekey.len Return new pair(thekey,blu.ToText+"-"+finalposition.totext) end if end next Return nil
Adding some style
Finally, we need a way to inform the user that the word down the pointer cursor is active so she can click on it. For this example I’ve used the Underline text style, but you can adapt it once you see how this is done. So, let’s add the last method for the class using the following data:
- Method Name: setStyleForWord
- Parameters: word As pair, mode As Boolean.
- Scope: Private.
We use the mode
parameter so we can use the method to apply or delete the style to the received word; removing for example the style from the previous detected active word and styiling the new one. Write the following code in the associated Code Editor:
Dim cStart, cEnd, sStart As Double cStart = NthField(word.Right,"-",1).Val cEnd = NthField(word.Right,"-",2).Val - cStart Dim t As StyledText t = Me.StyledText cstart = If( cstart - 1 < 0, 0, cstart-1) t.Underline(cstart, cEnd) = mode
Putting it all together!
With our 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 in order to create a new instance. Use the Inspector to change its name to myURLField
. Then add three Label controls from the Library and an HTMLViewer. 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 be something like this:
Where the text under the “Sample Text” label is the one assigned to the Text
property of the LinkDetectorTextArea
instance.
Let’s add now to the Window1
object the method that will act as an observer, so 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 now the Open
event to the Window1
window, adding the following code to the associated Code Editor. Here is where we will initialize our LinkDetectorTextArea
instance providing the clickable words and their associated URL:
myURLField.setDictionary New Dictionary("Xojo":"http://www.xojo.com","Web":"https://www.w3.org","iOS":"http://www.apple.com/ios/",_"OS X":"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":"http://www.aprendexojo.com") myURLField.registerObserver WeakAddressOf updateContents
Run the project, move the pointer cursor over the text and you will see the how the detected active words are underlined on the fly; and if you click on any of them, then the Window will display the detected word, loading the associated URL.
Of course, this can be vastly improved…and that is something that maybe you can do in any number of ways!
Javier Rodriguez has been the Xojo Spanish Evangelist since 2008, he’s also a Developer, Consultant and Trainer who has be using Xojo since 1998. He manages AprendeXojo.com and is the developer behind the GuancheMOS plug-in for Xojo Developers, Markdown Parser for Xojo, HTMLColorizer for Xojo and the Snippery app, among others