|
Now that you have enough background knowledge for writing a custom control, you can start by learning about the StyledTextArea control's specification. I first describe the specification of the StyledTextArea control so that you know exactly what to expect. Then, I present the class diagram and detail all the classes and structures that make up the control.
The StyledTextArea control is a text area, similar to the RichTextBox control, with the following features:
Each character occupies the same width and height.
Each line can have an unlimited number of characters.
A blinking caret indicates the character insertion point. The column and line numbers of the text area specify the insertion point. Both column and line numbers are 1-based. Therefore, the character on the second line and at the first column is at position (1, 2).
The user can press the arrow keys to move the caret in any of the four directions, effectively moving the insertion point. Pressing the left arrow key when the caret is on the first column moves the caret to the last column of the previous line, if any. Pressing the right arrow key when the caret is on the last column moves the caret to the first column of the next line. Pressing the down arrow key moves the caret to the next line of the same column. If the next line has fewer characters than the column number, the caret moves to the right of the last character of the next line. By the same token, pressing the up arrow key moves the caret to the previous line of the same column. If the previous line has fewer characters than the column number, the caret moves to the right of the last character of the previous line.
The user can press the backspace key to delete the character to the left of the caret. If the caret is on the first column of a line, the caret moves to the last character of the previous line and concatenates the previous line with the line that the caret is on before the backspace key is pressed. All the lines after the caret location will then move up by one line.
The user can press the Delete key to delete the character to the right of the caret. All the remaining characters in the same line will then shift one character to the left. If the user presses the Delete key when the caret is on the last column of a line, the next line joins the current line, and all the lines next to it move up by one line.
The text area ignores the pressing of a character or a combination of characters that it cannot display. For example, pressing Ctrl+A does not have any effect.
The StyledTextArea component has a vertical scrollbar as well as a horizontal scrollbar to scroll the text area.
The StyledTextArea component raises the ColumnChanged event when the caret is moved to another column and raises the LineChanged event when the caret moves to another line.
The user can select part of the text on the text area by dragging the mouse over the text.
The user can cut and copy selected text into the Clipboard programmatically.
The user can paste the contents of the Clipboard into the text area as long as the content is in text format. The text area, for example, cannot paste an image.
Figure 1-5 shows the StyledTextArea component in a form.
Figure 1-5: The StyledTextArea component
Figure 1-6 shows the class diagram for the StyledTextArea component.
Figure 1-6: The StyledTextArea class diagram
The StyledTextArea component implements a MVC pattern with two views. It has the following main classes:
Model represents the model of the component.
View represents the screen to display the data of the model.
LineNumberView is another view visualizing the line numbers of the model.
StyledTextArea is the controller.
Note | All the classes comprising the StyledTextArea control are part of the default namespace.You can find all the classes in the SytledTextArea.vb file in the project's directory. |
Instances of the Model, View, and LineNumberView classes are created when the StyledTextArea class is constructed. The following InitializeComponent method of the StyledTextArea class describes the interrelationships between the four main objects:
model = New Model() view = New View(model) ' Me is the StyledTextArea object, ' so we are passing the controller to the view view.controller = Me lineNumberView = New LineNumberView(model) lineNumberView.controller = Me ' pass the controller to the lineNumberView
Both the controller (StyledTextArea) and the two views (View and LineNumberView) receive the same instance of the Model class. In addition, both views also get the reference to the controller.
Note | In addition to the four main classes, there are also supporting small types: the ColumnLine structure, some delegates, and some event argument classes. I first discuss these supporting classes because they are much simpler, and then I talk about the four main classes. Each type has a section of its own. |
The column number and the line number on which the character lies denote the character's location. The ColumnLine structure represents this column-line coordinate. This structure is similar to the System.Drawing.Point structure. However, the Point structure normally represents a location in a drawing area and is in pixels.
The ColumnLine structure is as follows:
Public Structure ColumnLine Public Column As Integer Public Line As Integer Public Sub New(ByVal column As Integer, ByVal line As Integer) Me.Column = column Me.Line = line End Sub End Structure
The ColumnLine structure has two public integers that represent a column and a line, respectively.
By making ColumnLine a structure rather than a class, ColumnLine will automatically inherit the System.ValueType class and not the System.Object class. The System.ValueType class's Equals method supports value equality, which is inherited by the ColumnLine structure. Therefore, if cl1 and cl2 reference two different ColumnLine objects and both cl1 and cl2 have the same column and line values, cl1.Equals(cl2) and cl2.Equals(cl1) will return True.
On the other hand, the Equals method in the System.Object class only supports reference equality. The two object references a and b are only equal if they reference the same instance.
If you use a class and not a structure, you have to override the Equals method, as shown in Listing 1-5.
Listing 1-5: Overriding Equals
Public Overloads Overrides Function Equals(ByVal obj As Object) As Boolean If obj Is Nothing Then Return False End If If TypeOf obj Is ColumnLine Then Dim cl As ColumnLine = CType(obj, ColumnLine) If cl.Column = Column And cl.Line = Line Then Return True Else Return False End If Else Return False End If End Function
The Model class can raise two events: when the number of lines changes and when the number of characters of the longest line changes. In addition, the StyledTextArea class can raise two events: when the caret moves to another column and when the caret moves to another line. For these four events, you declare the four delegates as in Listing 1-6.
Listing 1-6: Declaring the Delegates
Public Delegate Sub ColumnEventHandler(ByVal sender As Object, _ ByVal e As ColumnEventArgs) Public Delegate Sub LineEventHandler(ByVal sender As Object, _ ByVal e As LineEventArgs) Public Delegate Sub LineCountEventHandler(ByVal sender As Object, _ ByVal e As LineCountEventArgs) Public Delegate Sub LongestLineEventHandler(ByVal sender As Object, _ ByVal e As LongestLineEventArgs)
I give you more details on these delegates when discussing the Model and StyledTextArea classes.
Each of the four events that can be raised from within the Model class and the StyledTextArea class passes different data. The data is encapsulated by the following four classes that all inherit the System.EventArgs class: ColumnEventArgs, LineEventArgs, LineCountEventArgs, and LongestLineEventArgs.
Listing 1-7 shows the ColumnEventArgs class.
Listing 1-7: ColumnEventArgs
Public Class ColumnEventArgs : Inherits EventArgs Private oldColumnField, newColumnField As Integer Public Sub New(ByVal oldColumn As Integer, ByVal newColumn As Integer) oldColumnField = oldColumn newColumnField = newColumn End Sub Public ReadOnly Property OldColumn() As Integer Get Return oldColumnField End Get End Property Public ReadOnly Property NewColumn() As Integer Get Return newColumnField End Get End Property End Class
You construct a ColumnEventArgs object by passing two integers to its constructor: the value for the old column and the value for the new column. An event receiver can then obtain these two values from the OldColumn and NewColumn properties.
Listing 1-8 shows the LineEventArgs class.
Listing 1-8: LineEventArgs
Public Class LineEventArgs : Inherits EventArgs Private oldLineField, newLineField As Integer Public Sub New(ByVal oldLine As Integer, ByVal newLine As Integer) oldLineField = oldLine newLineField = newLine End Sub Public ReadOnly Property OldLine() As Integer Get Return oldLineField End Get End Property Public ReadOnly Property NewLine() As Integer Get Return newLineField End Get End Property End Class
You construct a LineEventArgs object by passing two integers to its constructor: the value for the old line and the value for the new line. An event receiver can then obtain these two values from the OldLine and NewLine properties.
Listing 1-9 shows the LineCountEventArgs class.
Listing 1-9: LineCountEventArgs
Public Class LineCountEventArgs : Inherits EventArgs Private lineCountField As Integer ' the current line count Public Sub New(ByVal lineCount As Integer) lineCountField = lineCount End Sub Public ReadOnly Property LineCount() As Integer Get Return lineCountField End Get End Property End Class
You can instantiate a LineCountEventArgs object by passing an integer to its constructor: the new line count. An event receiver can then obtain this value from the LineCount property.
Listing 1-10 shows the LongestLineEventArgs class.
Listing 1-10: LongestLineEventArgs
Public Class LongestLineEventArgs : Inherits EventArgs Private charCountField As Integer Public Sub New(ByVal charCount As Integer) charCountField = charCount End Sub Public ReadOnly Property LongestLineCharCount() As Integer Get Return charCountField End Get End Property End Class
You instantiate a LongestLineEventArgs object by passing an integer to its constructor: the new longest line character count. An event receiver can then obtain this value from the LongestLineCharCount property.
The data in the Model class is a collection of characters entered by the user. These characters are grouped into strings. The first string comprises all characters from the first character to the character before the first carriage-return character. The character after the carriage-return character is the first character of the second string, and this string spans until the character just before the second carriage- return character, and so on. The last string's element starts from the character after the last carriage-return character to the last character the user entered.
Consider the following set of characters in which \n represents a carriage- return character. The user of the StyledTextArea component has input this series of characters:
The speaker was\nthe well-known\nAnna Pavlovna Scherer
These characters will be grouped into three strings. The first string will have the value of The speaker was, the second will have the value of the well-known, and the third will have the value of Anna Pavlovna Scherer. Note that the carriage- return characters are not part of the string objects.
In the StyledTextArea component, the public class Model represents the model:
Public Class Model ... End Class
The data holder in the Model class is a System.Collections.ArrayList named list. Each member of the ArrayList is a String object. The ArrayList appears in the declaration section of the Model class:
Private list As New ArrayList(1024)
In this example, you declare and instantiate list in the same line by passing the number of initial members to its constructor. The choice of 1,024 is arbitrary, and the ArrayList will grow automatically when the number of members exceeds 1,024. In theory, list can contain an unlimited number of lines.
The Model class has another field: longestLineCharCountField, which holds the value for the LongestLineCharCount property:
Private longestLineCharCountField As Integer
When instantiated, the Model object will add an empty string as its first member. The behavior of Model is that it will have at least one member. When all characters in the ArrayList are deleted, the first empty string will remain.
You add an empty string in the Model class's constructor:
Public Sub New() list.Add("") End Sub
The Model class provides different public properties and methods for the view and controller to flexibly access its data. In addition, the Model class can raise four events. The following sections describe the properties, methods, and events.
The Model class has three public properties: CharCount, LineCount, and LongestLineCharCount.
The CharCount read-only property returns the number of characters in the Model class (see Listing 1-11). Bear in mind that the Model instance does not store carriage-return characters; therefore they are not counted.
Listing 1-11: CharCount
Public ReadOnly Property CharCount() As Integer ' the total of lengths of all lines, therefore newline is not counted Get Dim i, total As Integer For i = 0 To LineCount - 1 total = total + list.Item(i).ToString().Length Next i Return total End Get End Property
The number of characters is calculated every time the CharCount property is called. The return value is the total number of characters in all the strings in the ArrayList.
The LineCount read-only property returns the number of lines in the Model object—in other words, the number of members of the ArrayList named list. The implementation of this property is as follows:
Public ReadOnly Property LineCount() As Integer Get Return list.Count End Get End Property
The LongestLineCharCount read-only property returns the number of characters in the longest string (see Listing 1-12). This is useful because the controller (the StyledTextArea class) uses a horizontal scrollbar whose maximum and value change dynamically according to the number of characters in the longest string.
Listing 1-12: LongestLineCharCount
Public ReadOnly Property LongestLineCharCount() As Integer Get Dim lineCount As Integer = list.Count If lineCount = 0 Then Return 0 Else Dim i, max As Integer For i = 0 To lineCount - 1 Dim thisLineCharCount As Integer = CType(list.Item(i), String).Length If thisLineCharCount > max Then max = thisLineCharCount End If Next Return max End If End Get End Property
The getter of the LongestLineCharCount property works by checking the length of each string in the ArrayList.
The Model class has several methods you can use to manipulate the data in the Model object.
The DeleteChar method deletes a character from the location indicated by the ColumnLine structure (see Listing 1-13). If the longest line's character count changes because of the deletion, this method raises the LongestLineCharCountChanged event.
Listing 1-13: DeleteChar
Public Sub DeleteChar(ByVal deleteLocation As ColumnLine) Dim oldLongestLineCharCount As Integer = LongestLineCharCount list.Item(deleteLocation.Line - 1) = _ GetLine(deleteLocation.Line).Remove(deleteLocation.Column - 1, 1) Dim newLongestLineCharCount As Integer = LongestLineCharCount If oldLongestLineCharCount <> newLongestLineCharCount Then OnLongestLineCharCountChanged(New _ LongestLineEventArgs(newLongestLineCharCount)) End If End Sub
You delete characters by assigning a new string to a specified item of the ArrayList named list. The new string is an old string whose character at the specified column has been removed using the Remove method of the String class. The GetLine method obtains the specified line from which a character needs to be removed:
list.Item(deleteLocation.Line - 1) = _ GetLine(deleteLocation.Line).Remove(deleteLocation.Column - 1, 1)
Before and after the deletion, the LongestLineCharCount property is called and its value is stored in oldLongestLineCharCount and newLongestLineCharCount (respectively):
Dim oldLongestLineCharCount As Integer = LongestLineCharCount . . . Dim newLongestLineCharCount As Integer = LongestLineCharCount
If newLongestLineCharCount and oldLongestLineCharCount are different after the character deletion, the LongestLineCharCountChanged method is called, which in turns raises the LongestLineCharCountChanged event:
If oldLongestLineCharCount <> newLongestLineCharCount Then OnLongestLineCharCountChanged(New _ LongestLineEventArgs(newLongestLineCharCount)) End If
The GetLine method returns the line at the specified line number. Line numbers are 1-based—in other words, the first line is line number 1. This method throws an ArgumentOutOfRangeException if the specified line is a negative number or greater than the number of members in the ArrayList named list.
The implementation of the GetLine method is as follows:
Public Function GetLine(ByVal lineNo As Integer) As String 'lineNo is 1-based If lineNo > 0 And lineNo <= list.Count Then Return CType(list.Item(lineNo - 1), String) Else Throw New ArgumentOutOfRangeException() End If End Function
The InsertChar method inserts a character at the specified ColumnLine (see Listing 1-14). If the longest line's character count changes because of the insertion, this method raises the LongestLineCharCountChanged event.
Listing 1-14: InsertChar
Public Sub InsertChar(ByVal c As Char, ByVal insertLocation As ColumnLine) Dim oldLongestLineCharCount As Integer = LongestLineCharCount Dim oldLine As String = list.Item(insertLocation.Line - 1).ToString() Dim newLine As String = _ oldLine.Insert(insertLocation.Column - 1, c.ToString()) list.Item(insertLocation.Line - 1) = newLine Dim newLongestLineCharCount As Integer = LongestLineCharCount If oldLongestLineCharCount <> newLongestLineCharCount Then OnLongestLineCharCountChanged(New _ LongestLineEventArgs(newLongestLineCharCount)) End If End Sub
You insert a character into a string using the Insert method of the String class at the specified index. You obtain the string into which a character is inserted using the Item property of the ArrayList class:
Dim oldLine As String = list.Item(insertLocation.Line - 1).ToString() Dim newLine As String = _ oldLine.Insert(insertLocation.Column - 1, c.ToString())
The new line then replaces the old line:
list.Item(insertLocation.Line - 1) = newLine
Before and after the insertion, the LongestLineCharCount property is called and its value is stored in oldLongestLineCharCount and newLongestLineCharCount (respectively):
Dim oldLongestLineCharCount As Integer = LongestLineCharCount . . . Dim newLongestLineCharCount As Integer = LongestLineCharCount
If newLongestLineCharCount and oldLongestLineCharCount are different after the character insertion, the LongestLineCharCountChanged method is called, which in turns raises the LongestLineCharCountChanged event:
If oldLongestLineCharCount <> newLongestLineCharCount Then OnLongestLineCharCountChanged(New _ LongestLineEventArgs(newLongestLineCharCount)) End If
The InsertData method inserts a string at the specified ColumnLine (see Listing 1-15). The string can contain carriage-return and line-feed characters after the string is separated into multiple shorter strings at every occurrence of a carriage-return character. Line-feed characters, if they exist, will be removed prior to insertion. The method can raise the LongestLineCharCountChanged event as well as the LineCountChanged event.
Listing 1-15: InsertData
Public Function InsertData(ByVal data As String, _ ByVal insertLocation As ColumnLine) As ColumnLine 'delete vbLf character data = data.Replace(Microsoft.VisualBasic.Constants.vbLf.ToString(), "") Dim initialLongestLineCharCount As Integer = LongestLineCharCount Dim x As Integer = insertLocation.Column Dim y As Integer = insertLocation.Line Dim returnInserted As Boolean ' data may contain carriage return character Dim thisLine As String = GetLine(y) Dim head As String = thisLine.Substring(0, x - 1) Dim tail As String = thisLine.Substring(x - 1) list.RemoveAt(y - 1) Dim startIndex As Integer Do While (startIndex >= 0) Dim endIndex As Integer = _ data.IndexOf(Microsoft.VisualBasic.Constants.vbCr, startIndex) Dim line As String If endIndex = -1 Then line = data.Substring(startIndex) 'don't use SetLine bec it can raise event Dim newLine As String = head & line & tail list.Insert(y - 1, newLine) x = head.Length + line.Length + 1 startIndex = endIndex Else line = data.Substring(startIndex, endIndex - startIndex) list.Insert(y - 1, head & line) returnInserted = True y = y + 1 x = 1 head = "" startIndex = endIndex + 1 'without carriage return End If Loop Dim currentCharCount As Integer = LongestLineCharCount If initialLongestLineCharCount <> currentCharCount Then OnLongestLineCharCountChanged(New LongestLineEventArgs(currentCharCount)) End If If returnInserted Then OnLineCountChanged(New LineCountEventArgs(LineCount)) End If Return New ColumnLine(x, y) End Function
The Paste method of the StyledTextArea class calls this method, which returns the new location of the insertion point that will be used by the StyledTextArea object to move the caret.
The InsertData method first removes any line-feed character using the Replace method of the String class:
data = data.Replace(Microsoft.VisualBasic.Constants.vbLf.ToString(), "")
It then retrieves the number of characters of the longest line prior to insertion:
Dim initialLongestLineCharCount As Integer = LongestLineCharCount
The Boolean returnInserted acts as a flag to indicate the presence of a carriage-return character in the String argument data:
Dim returnInserted As Boolean
It then obtains the line into which data will be inserted. The substring before the insertion point and the substring after the insertion point are also stored into head and tail:
Dim thisLine As String = GetLine(y) Dim head As String = thisLine.Substring(0, x - 1) Dim tail As String = thisLine.Substring(x - 1)
The method then deletes the line into which data will be inserted:
list.RemoveAt(y - 1)
The String argument data is then divided into smaller strings in which a carriage-return character denotes the end of a string. Each resulting string is then inserted into the ArrayList named list:
Dim startIndex As Integer Do While (startIndex >= 0) Dim endIndex As Integer = _ data.IndexOf(Microsoft.VisualBasic.Constants.vbCr, startIndex) Dim line As String If endIndex = -1 Then ' last line line = data.Substring(startIndex) 'don't use SetLine bec it can raise event Dim newLine As String = head & line & tail list.Insert(y - 1, newLine) x = head.Length + line.Length + 1 startIndex = endIndex Else line = data.Substring(startIndex, endIndex - startIndex) list.Insert(y - 1, head & line) returnInserted = True y = y + 1 x = 1 head = "" startIndex = endIndex + 1 'without carriage return End If Loop
Note that the returnInserted flag will be True if there is a carriage-return character in the data.
The method then checks if the longest line's character count has changed. If so, it raises LongestLineCharCountChanged by calling the OnLongesLineChar-CountChanged method:
Dim currentCharCount As Integer = LongestLineCharCount If initialLongestLineCharCount <> currentCharCount Then OnLongestLineCharCountChanged(New LongestLineEventArgs(currentCharCount)) End If
It also checks if the number of lines has changed by checking the returnInserted Boolean. If it has, it raises the LineCountChanged event by calling the OnLineCountChanged method:
If returnInserted Then OnLineCountChanged(New LineCountEventArgs(LineCount)) End If
Finally, it returns a new ColumnLine object representing the insertion point after the data insertion:
Return New ColumnLine(x, y)
The InsertLine method inserts a line at the specified line number. It raises the LineCountChanged event and can raise the LongestLineCharCountChanged event:
Public Sub InsertLine(ByVal lineNo As Integer, ByVal line As String) If lineNo > 0 And lineNo <= list.Count + 1 Then Dim oldLongestLineCharCount As Integer = LongestLineCharCount list.Insert(lineNo - 1, line) OnLineCountChanged(New LineCountEventArgs(LineCount)) Dim newLongestLineCharCount As Integer = LongestLineCharCount If oldLongestLineCharCount <> newLongestLineCharCount Then OnLongestLineCharCountChanged(New _ LongestLineEventArgs(newLongestLineCharCount)) End If End If End Sub
Note that the LongestLineCharCount property is called before and after the line is inserted. If the property value is different after the insertion, the LongestLineCharCountChanged event is raised by calling the OnLongestLineCharCountChanged method.
OnColumnChanged is a protected method used internally to raise the ColumnChanged event:
Protected Overridable Sub OnColumnChanged(ByVal e As ColumnEventArgs) RaiseEvent ColumnChanged(Me, e) End Sub
OnLineChanged is a protected method used internally to raise the LineChanged event:
Protected Overridable Sub OnLineChanged(ByVal e As LineEventArgs) RaiseEvent LineChanged(Me, e) End Sub
OnLineCountChanged is a protected method used internally to raise the LineCountChanged event:
Protected Overridable Sub OnLineCountChanged(ByVal e As LineCountEventArgs) RaiseEvent LineCountChanged(Me, e) End Sub
OnLongestLineCharCountChanged is a protected method used internally to raise the LongestLineCharCount event:
Protected Overridable Sub OnLongestLineCharCountChanged( _ ByVal e As LongestLineEventArgs) RaiseEvent LongestLineCharCountChanged(Me, e) End Sub
RemoveLine removes a line at the specified line number. This method accepts a second argument that is a Boolean. A value of False for this argument will prevent any event to be raised from inside this method. Otherwise, it raises the LineCountChanged event and can raise the LongestLineCharCount event. Its implementation is as follows:
Public Sub RemoveLine(ByVal lineNo As Integer, ByVal triggerEvent As Boolean) If lineNo > 0 And lineNo <= list.Count Then Dim oldLongestLineCharCount As Integer = LongestLineCharCount list.RemoveAt(lineNo - 1) If triggerEvent Then Dim newLongestLineCharCount As Integer = LongestLineCharCount If oldLongestLineCharCount <> newLongestLineCharCount Then OnLongestLineCharCountChanged(New _ LongestLineEventArgs(newLongestLineCharCount)) End If OnLineCountChanged(New LineCountEventArgs(LineCount)) End If End If End Sub
SetLine changes the line at the specified line number with the specified string. It can raise the LongestLineCharCountChanged event. Its implementation is as follows:
Public Sub SetLine(ByVal lineNo As Integer, ByVal line As String) Dim oldLongestLineCharCount As Integer = LongestLineCharCount If (lineNo > 0 And lineNo <= list.Count) Then list.Item(lineNo - 1) = line End If Dim newLongestLineCharCount As Integer = LongestLineCharCount If oldLongestLineCharCount <> newLongestLineCharCount Then OnLongestLineCharCountChanged(New _ LongestLineEventArgs(newLongestLineCharCount)) End If End Sub
The Model class contains several events.
LineCountChanged raises when the number of lines—in other words, the number of members of the ArrayList list—changes. Its signature is as follows:
Public Event LineCountChanged As LineCountEventHandler
LongestLineCharCountChanged raises when the number of characters of the longest line changes:
Public Event LongestLineCharCountChanged As LongestLineEventHandler
The View class represents a screen that displays the data (the collection of string lines) in the Model object. As such, the View must reference the instance of the Model object. In addition, the View has a reference to the controller object so that it can communicate with the controller the way it should display the data. For example, the controller has the variable named TopInvisibleLineCount, which contains the number of lines that should be skipped because the user has scrolled down the screen. When displaying the data, the view needs to know the value of TopInvisibleLineCount so that it can display the correct data.
You declare these object references as follows:
Public controller As StyledTextArea Public model As model
There is also a variable named LeftInvisibleCharCount, which contains the number of invisible characters to the left of the screen:
Public LeftInvisibleCharCount As Integer
The value of LeftInvisibleCharCount changes when the user continues typing after the caret reaches the rightmost character space of the screen. To make what is being typed visible, the screen must scroll to the left. The value of LeftInvisibleCharCount also changes when the user moves the horizontal scrollbar of the controller.
Note | LeftInvisibleCharCount is actually similar to the controller's TopInvisibleLineCount. However, although LeftInvisibleCharCount exists inside the View class, TopInvisibleLineCount resides in the controller. The latter has to be in the controller because this value is also needed by the second view, LineNumberView. By keeping TopInvisibleLineCount in the controller, both views do not have to know each other. On the other hand, only the View class uses LeftInvisibleCharCount. The LineNumberView class does not need it. |
The declaration part of the View class also contains variables that define how each character should be displayed. For example, lineSpace defines the number of pixels that should separate two lines:
Public lineSpace As Integer = 2 ' number of pixels between 2 lines
Also, fontFace determines the font type used to draw characters, and the characterWidth value represents the number of pixels occupied by the width of every character:
Public fontFace As String = "Courier New" Public characterWidth As Integer = 8
As for the character's font height, you use the FontHeight property inherited from the Control class, so you do not need a variable for it.
The StyledTextArea control allows the user to select part of or the whole text. The selected text will be painted in different colors defined by highlightBackColor and highlightForeColor:
Public highlightBackColor As Color = Color.DarkBlue Public highlightForeColor As Color = Color.White
For the caret, the caretThread thread makes the caret blink:
Private caretThread As Thread
To indicate whether at one instance the caret is visible or invisible, you use a Boolean called caretVisible. The value of caretVisible toggles each time the method that draws the caret is called, giving the caret the needed blinking effect:
Private caretVisible As Boolean = True
Finally, to draw the caret, you use a Pen object called pen with a width of penWidth:
Private penWidth As Integer = 2 Private pen As New Pen(Color.Black, penWidth)
To change the width of the caret, you can change the value of penWidth. To change the color, you can change the Color property of the Pen object.
Because a view is useless unless it has a reference to the Model object, the View class's only constructor accepts one argument of type Model, which is then passed to the model object variable:
Public Sub New(ByRef model As Model) Me.model = model . . . End Sub
Afterward, the Font property is instantiated. This font draws each line of strings in the Model object:
fontHeight = 10 Font = New Font(fontFace, fontHeight)
The last piece of the code in the View class's constructor creates a System.Threading.Thread object, which manages the caret. You instantiate it by passing a ThreadStart delegate. This delegate references the methods to be invoked when this thread begins executing:
caretThread = New Thread(New ThreadStart(AddressOf DisplayCaret))
You then start the thread by calling its Start method:
caretThread.Start()
When the View object is destroyed, this thread must also be destroyed. Otherwise, the whole application (the form that incorporates the StyledTextArea control) cannot exit properly. To terminate the thread when the View object is destroyed, the View class overrides the Dispose method as follows:
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) caretThread.Abort() caretThread.Join() MyBase.Dispose(disposing) End Sub
The following sections discuss each property in the View class.
The CaretColor property specifies the color of the caret:
Public Property CaretColor() As Color Get Return pen.Color End Get Set(ByVal Value As Color) pen.Color = Value End Set End Property
You can change the caret color by changing the value of the Color property of the Pen object used to draw the caret.
The read-only VisibleCharCount property returns the number of characters that can span along the width of the screen:
Public ReadOnly Property VisibleCharCount() As Integer Get Return CInt(Math.Floor(Me.Width / characterWidth)) - 1 End Get End Property
The read-only VisibleLineCount property returns the number of lines that can be displayed across the height of the screen:
Public ReadOnly Property VisibleLineCount() As Integer Get Return CInt(Me.Height / (lineSpace + GetFontHeight())) End Get End Property
The number of visible lines in the view is the height of the view control divided by the number of pixels between lines plus the font height.
The View class has the following methods.
DisplayCaret is a private method that is passed to the thread dedicated to making the caret flash on and off (caretThread). This method consists of an indefinite While loop that keeps on running as long as caretThread is still alive. The main thing this method does is toggle the caretVisible Boolean variable. The DrawCaret method then uses this variable to determine whether the caret should be visible at that instance. The other thing the method does is invalidate and update the region occupied by the caret to create the blinking effect of the caret.
The method implementation is as follows:
Private Sub DisplayCaret() Try While True ' call DrawCaret here Dim caretsLine As String = model.GetLine(controller.CaretLineNo) Dim x As Integer = GetStringWidth(caretsLine.Substring(0, _ controller.CaretColumnNo - 1)) + _ penWidth - (LeftInvisibleCharCount * characterWidth) Dim y As Integer = (controller.CaretLineNo - 1 - _ controller.TopInvisibleLineCount) * (lineSpace + fontHeight) Dim caretRectangle As New Rectangle( _ x - penWidth, y, 2 * penWidth, lineSpace + fontheight) Me.Invalidate(caretRectangle) Me.Update() If Not caretVisible Then Thread.Sleep(150) Else Thread.Sleep(350) End If caretVisible = Not caretVisible End While Catch End Try End Sub
The coordinates x and y give the left-top point on the text area where the caret should be drawn. Note also that the thread is put to sleep for two different periods of time. When the caret is visible—in other words, when caretVisible is True—the Thread class's Sleep method is given the value 350 so that the caret will stay visible for 350 milliseconds. Conversely, when the caret is not visible, the Sleep method is given the value 150.
The Dispose method is a protected method that overrides the Dispose method in the Control class and is used internally to ensure that the caret thread is terminated when the instance of this View object is destroyed. Its implementation is as follows:
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) caretThread.Abort() caretThread.Join() MyBase.Dispose(disposing) End Sub
The DrawCaret method actually draws the caret. The DisplayCaret method invalidates and updates the text area, causing the OnPaint method to be called. Toward the end of the OnPaint method, the OnPaint method calls the DrawCaret method to draw the caret. The DrawCaret method implementation is as follows:
Protected Sub DrawCaret(ByRef graphics As Graphics) 'it's protected so that it can be overriden by subclass If caretVisible And Me.Focused Then 'Measure string Dim caretsLine As String = model.GetLine(controller.CaretLineNo) Dim x As Integer = GetStringWidth(caretsLine.Substring(0, _ controller.CaretColumnNo - 1)) + _ penWidth - (LeftInvisibleCharCount * characterWidth) Dim y As Integer = (controller.CaretLineNo - 1 - _ controller.TopInvisibleLineCount) * (lineSpace + fontHeight) graphics.DrawLine(pen, x, y, x, y + lineSpace + fontHeight) End If End Sub
The first thing the DrawCaret method does is to check the value of caretVisible, a Boolean that is constantly being toggled by the DisplayCaret method. When the caretVisible is True, the DrawCaret method obtains the starting point for the caret, as in the following code:
Dim caretsLine As String = model.GetLine(controller.CaretLineNo) Dim x As Integer = GetStringWidth(caretsLine.Substring(0, _ controller.CaretColumnNo - 1)) + _ penWidth - (LeftInvisibleCharCount * characterWidth) Dim y As Integer = (controller.CaretLineNo - 1 - _ controller.TopInvisibleLineCount) * (lineSpace + fontHeight)
It then uses x and y to draw the caret, using the DrawLine method of the Graphics object passed as the argument:
graphics.DrawLine(pen, x, y, x, y + lineSpace + fontHeight)
The GetCaretXAbsolutePosition method obtains the x coordinate of the caret position in pixels:
Public Function GetCaretXAbsolutePosition() As Integer Dim caretsLine As String = controller.GetCurrentLine() If Not caretsLine Is Nothing Then Return _ GetStringWidth(caretsLine.Substring(0, controller.CaretColumnNo - 1)) Else Return 0 End If End Function
It first gets the current line and then returns the result of the GetStringWidth method.
The GetFontHeight method returns the value of the FontHeight property:
Public Function GetFontHeight() As Integer Return FontHeight End Function
The GetStringWidth method returns the number of pixels that the specified string will occupy when drawn on the text area:
Private Function GetStringWidth(ByRef s As String) As Integer If Not s Is Nothing Then Return s.Length * characterWidth Else Return 0 End If End Function
The string width is easy to calculate because each character is given the same width—in other words, the font is forced to be monospaced.
The IsCaretVisible method indicates whether the caret is visible. The caret can become invisible if the text area is scrolled because scrolling retains the caret at its relative position. The implementation of the IsCaretVisible method is as follows:
Public Function IsCaretVisible() As Boolean Dim xPosition As Integer = GetCaretXAbsolutePosition() Dim leftInvisibleWidth As Integer = LeftInvisibleCharCount * characterWidth If xPosition < leftInvisibleWidth Or _ xPosition > leftInvisibleWidth + Me.Width - 5 Or _ controller.CaretLineNo > _ controller.TopInvisibleLineCount + VisibleLineCount Then Return False Else Return True End If End Function
The LeftInvisibleCharCount variable contains the number of characters that are not visible because the text area has been scrolled to the left. The number of pixels the invisible characters take (leftInvisibleWidth) is the product of LeftInvisibleCharCount and the width of each character in pixels. The caret is invisible if the x coordinate of the caret position is less than leftInvisibleWidth or greater than leftInvisibleWidth plus the text area width plus 5. The caret can also become invisible if the line number of the line the caret is on (controller.CaretLineNo) is greater than TopInvisibleLineCount plus VisibleLineCount.
The MoveScreen method scrolls the text area's x position to the right, where x is the value passed to the method. A positive x means that the text area should be scrolled to the right, and a negative value indicates scrolling to the left. The method implementation is as follows:
Public Sub MoveScreen(ByVal increment As Integer) 'move screen horizontally by x character LeftInvisibleCharCount = Math.Max(LeftInvisibleCharCount + increment, 0) RedrawAll() End Sub
The OnPaint method provides the GUI for the text area. It draws all the characters visible at this instance and the caret. It also calls the PaintSelectionArea method if the controller has some text selected. The implementation of the OnPaint method is as follows:
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim graphics As Graphics = e.Graphics Dim textBrush As SolidBrush = New SolidBrush(ForeColor) Dim highlightTextBrush As SolidBrush = New SolidBrush(highlightForeColor) If controller.HasSelection() Then PaintSelectionArea(graphics) End If Dim i, visibleLine As Integer Dim maxCharCount As Integer For i = 1 To model.LineCount Dim thisLine As String = model.GetLine(i) If i > controller.TopInvisibleLineCount Then Dim y As Integer = visibleLine * (lineSpace + fontHeight) Dim j As Integer For j = 1 To thisLine.Length If j > LeftInvisibleCharCount And _ j <= LeftInvisibleCharCount + VisibleCharCount + 1 Then Dim x As Integer = GetStringWidth(thisLine.Substring(0, j - 1)) - _ LeftInvisibleCharCount * characterWidth If controller.IsInSelection(j, i) Then graphics.DrawString(thisLine.Substring(j - 1, 1), Font, _ highlightTextBrush, x, y) Else graphics.DrawString(thisLine.Substring(j - 1, 1), Font, _ textBrush, x, y) End If End If Next j visibleLine = visibleLine + 1 End If Next i 'draw caret here DrawCaret(graphics) End Sub
The OnPaint method starts by obtaining the Graphics object for the control (the View object) and constructing a SolidBrush to draw the normal text (textBrush) and a SolidBrush to draw the selected text (highlightTextBrush):
Dim graphics As Graphics = e.Graphics Dim textBrush As SolidBrush = New SolidBrush(ForeColor) Dim highlightTextBrush As SolidBrush = New SolidBrush(highlightForeColor)
Next, it calls the PaintSelectionArea method if the controller has selection—in other words, if part of the text is selected. The PaintSelectionArea method receives the reference to the previous Graphics object. The PaintSelectionArea method paints the background area for the selected text. However, it does not draw the selected text:
If controller.HasSelection() Then PaintSelectionArea(graphics) End If
Then, it draws each line of the data, character by character, using the DrawString method. Prior to drawing each character, the method checks whether the character is in the selection using the controller's IsInSelection method. If it is, it is drawn using the highlightTextBrush SolidBrush. Otherwise, it is drawn using the textBrush SolidBrush:
Dim i, visibleLine As Integer Dim maxCharCount As Integer For i = 1 To model.LineCount Dim thisLine As String = model.GetLine(i) If i > controller.TopInvisibleLineCount Then Dim y As Integer = visibleLine * (lineSpace + fontHeight) Dim j As Integer For j = 1 To thisLine.Length If j > LeftInvisibleCharCount And _ j <= LeftInvisibleCharCount + VisibleCharCount + 1 Then Dim x As Integer = GetStringWidth(thisLine.Substring(0, j - 1)) - _ LeftInvisibleCharCount * characterWidth If controller.IsInSelection(j, i) Then graphics.DrawString(thisLine.Substring(j - 1, 1), Font, _ highlightTextBrush, x, y) Else graphics.DrawString(thisLine.Substring(j - 1, 1), Font, _ textBrush, x, y) End If End If Next j visibleLine = visibleLine + 1 End If Next i
Finally, the OnPaint method calls the DrawCaret method:
DrawCaret(graphics)
The OnPaint method calls the PaintSelectionArea method to paint the background of the selected text:
Private Sub PaintSelectionArea(ByRef graphics As Graphics) Dim brush As New SolidBrush(highlightBackColor) ' representing start and end coordinates of selected text Dim x1, y1, x2, y2 As Integer Dim p1, p2 As ColumnLine p1 = controller.selectionStartLocation p2 = controller.selectionEndLocation x1 = p1.Column : y1 = p1.Line x2 = p2.Column : y2 = p2.Line If y1 > y2 Or (y1 = y2 And x1 > x2) Then 'swap Dim t As Integer t = y1 : y1 = y2 : y2 = t t = x1 : x1 = x2 : x2 = t End If Dim i As Integer Dim beginLine As Integer = Math.Max(y1, 1) Dim endLine As Integer = Math.Min(y2, model.LineCount) If beginLine = endLine Then If x1 > x2 Then Dim t As Integer t = x1 : x1 = x2 : x2 = t End If Dim thisLine As String = model.GetLine(beginLine) graphics.FillRectangle(brush, _ 2 + GetStringWidth(thisLine.Substring(0, x1 - 1)) - _ (LeftInvisibleCharCount * characterWidth), _ (beginLine - 1 - controller.TopInvisibleLineCount) * _ (lineSpace + fontHeight), _ GetStringWidth(thisLine.Substring(x1 - 1, x2 - x1)), _ (lineSpace + fontHeight)) Else For i = beginLine To endLine Dim thisLine As String = model.GetLine(i) If i = beginLine Then ' first line may not be the whole line, ' but from initial position of selection to end of string graphics.FillRectangle(brush, _ 2 + GetStringWidth(thisLine.Substring(0, x1 - 1)) - _ LeftInvisibleCharCount * characterWidth, _ (i - 1 - controller.TopInvisibleLineCount) * _ (lineSpace + fontHeight), GetStringWidth(thisLine) - _ GetStringWidth(thisLine.Substring(0, x1 - 1)), _ (lineSpace + fontHeight)) ElseIf i = endLine Then graphics.FillRectangle(brush, _ 2 - LeftInvisibleCharCount * characterWidth, _ (i - 1 - controller.TopInvisibleLineCount) * _ (lineSpace + fontHeight), _ GetStringWidth(thisLine.Substring(0, x2 - 1)), _ (lineSpace + fontHeight)) Else ' last line may not be the whole line, ' but from first column to initial position of selection graphics.FillRectangle(brush, _ 2 - LeftInvisibleCharCount * characterWidth, _ (i - 1 - controller.TopInvisibleLineCount) * _ (lineSpace + fontHeight), GetStringWidth(thisLine), _ (lineSpace + fontHeight)) End If Next i End If 'don't dispose graphics!! End Sub
The selection area is all the characters between two ColumnLine objects indicated by the controller's selectionStartLocation and selectionEndLocation variables. The background for the selected text is painted by drawing filled rectangles from the beginning of the selection toward the end. Rectangles are drawn in each line in the selection. Because the method always scans from the first visible line downward, it is important that the selectionEndLocation resides to the bottom of the selectionStartLocation or, if there is only one line selected, to the right of the selectionStartLocation. Therefore, before the painting, selectionStartLocation and selectionEndLocation are checked and swapped if necessary:
Dim x1, y1, x2, y2 As Integer Dim p1, p2 As ColumnLine p1 = controller.selectionStartLocation p2 = controller.selectionEndLocation x1 = p1.Column : y1 = p1.Line x2 = p2.Column : y2 = p2.Line If y1 > y2 Or (y1 = y2 And x1 > x2) Then 'swap Dim t As Integer t = y1 : y1 = y2 : y2 = t t = x1 : x1 = x2 : x2 = t End If
You can locate the end location at the top of the start location if the user drags the mouse upward when selecting the text.
You set the beginning line and the end line of the selection by using beginLine and endLine:
Dim beginLine As Integer = Math.Max(y1, 1) Dim endLine As Integer = Math.Min(y2, model.LineCount)
The next lines of code do the painting. A filled rectangle for a line is the result of calling the FillRectangle method of the Graphics class.
The RedrawAll method invalidates the whole text area and forces a repaint. Before invalidating, the method makes some necessary adjustments to the controller's TopInvisibleLineCount variable and its own LeftInvisibleCharCount variable. Its implementation is as follows:
Public Sub RedrawAll() 'before redraw correct invisible line count controller.TopInvisibleLineCount = _ Math.Min(controller.TopInvisibleLineCount, _ model.LineCount - VisibleLineCount) If controller.TopInvisibleLineCount < 0 Then controller.TopInvisibleLineCount = 0 End If LeftInvisibleCharCount = Math.Min(LeftInvisibleCharCount, _ model.LongestLineCharCount - VisibleCharCount) If LeftInvisibleCharCount < 0 Then LeftInvisibleCharCount = 0 Me.Invalidate() Me.Update() End Sub
The RepositionCaret method moves the caret to the new location when the user clicks the text area. This method accepts the x and y coordinates of the point where the user clicks on the text area. The method implementation is as follows:
Public Sub RepositionCaret(ByVal x As Integer, ByVal y As Integer) 'Get the (visible) line number Dim lineNumber As Integer = 1 + CInt(y / (fontHeight + lineSpace)) controller.CaretLineNo = Math.Min(lineNumber + _ controller.TopInvisibleLineCount, model.LineCount) 'Now calculate the closest position of the character in the current line Dim thisLine As String = controller.GetCurrentLine() Dim i As Integer, minDistance As Single = Width ' the width of this control Dim j As Integer = 0 For i = 0 To thisLine.Length Dim distance As Integer = _ Math.Abs(x + LeftInvisibleCharCount * characterWidth - _ GetStringWidth(thisLine.Substring(0, i))) If distance < minDistance Then minDistance = distance j = i End If Next i controller.CaretColumnNo = j + 1 RedrawAll() End Sub
You can calculate the y coordinate as follows:
Dim lineNumber As Integer = 1 + CInt(y / (fontHeight + lineSpace)) controller.CaretLineNo = Math.Min(lineNumber + _ controller.TopInvisibleLineCount, model.LineCount)
Note that if the user clicks on the area below the last line, the caret will move to the last line because the caret indicates the insertion point and you cannot insert a character into a nonexistent line.
The x coordinate of the caret has to be calculated so that the new location will be before the closest character to the point the user clicks:
Dim i As Integer, minDistance As Single = Width ' the width of this control Dim j As Integer = 0 For i = 0 To thisLine.Length Dim distance As Integer = _ Math.Abs(x + LeftInvisibleCharCount * characterWidth - _ GetStringWidth(thisLine.Substring(0, i))) If distance < minDistance Then minDistance = distance j = i End If Next i controller.CaretColumnNo = j + 1
Finally, the method calls the RedrawAll method to repaint the text area.
The Scroll method scrolls the text area vertically by x lines, where x is indicated by the value of the argument increment. The text area scrolls down if x is a positive integer and scrolls up for a negative x.
The method implementation is as follows:
Public Sub Scroll(ByVal increment As Integer) controller.TopInvisibleLineCount = _ controller.TopInvisibleLineCount + increment If controller.TopInvisibleLineCount < 0 Then controller.TopInvisibleLineCount = 0 End If RedrawAll() End Sub
You achieve scrolling by changing the value of the controller's TopInvisible- LineCount variable and calling the RedrawAll method.
The TranslateIntoCaretLocation method returns a ColumnLine object given a (x, y) coordinate of a point in the text area. This method is useful for determining what is the closest ColumnLine to the user click point. The method implementation is as follows:
Public Function TranslateIntoCaretLocation( _ ByVal x1 As Integer, ByVal y1 As Integer) As ColumnLine Dim column, line As Integer ' the coordinate for the returned Point 'set lowest value for y1 in case the use keeps dragging above the control If y1 < 1 Then y1 = 1 End If 'Get the visible line number line = Math.Min(1 + controller.TopInvisibleLineCount + _ CInt(y1 / (fontHeight + lineSpace)), model.LineCount) 'Now calculate the closest position of the character in the current line Dim thisLine As String = model.GetLine(line) Dim i As Integer, minDistance As Single = Me.Width 'the width of this control Dim j As Integer = 0 For i = 0 To thisLine.Length Dim distance As Single = _ Math.Abs(x1 + LeftInvisibleCharCount * characterWidth - _ GetStringWidth(thisLine.Substring(0, i))) If distance < minDistance Then minDistance = distance j = i End If Next i column = j + 1 Return New ColumnLine(column, line) End Function
The LineNumberView class represents a view that displays the line numbers of the Model object. The LineNumberView class has the model and controller object variables that are assigned to the Model object and the controller:
Private model As model Public controller As StyledTextArea
In addition, two more class variables determine how line numbers are displayed, lineSpace and fontFace:
Public lineSpace As Integer = 2 Public fontFace As String = "Courier New"
The integer lineSpace is the number of pixels between two line numbers, and fontFace is the font type used to draw the numbers.
The only constructor of the LineNumberView class accepts an argument of type Model. When an instance of this class is created, it assigns this object to its model variable. It also sets its FontHeight and Font properties. This is the LineNumberView class's constructor:
Public Sub New(ByRef model As Model) Me.model = model FontHeight = 10 Font = New Font(fontFace, FontHeight) End Sub
The LineNumberView class has one property: VisibleLineCount, which is similar to the VisibleLineCount property of the View class. The following is the property implementation:
Public ReadOnly Property VisibleLineCount() As Integer Get Return CInt(Me.Height / (lineSpace + FontHeight)) End Get End Property
The LineNumberView class has two methods: OnPaint and RedrawAll. The following sections explain both.
The OnPaint method overrides the OnPaint method in the Control class. Its mission is to provide a GUI for the LineNumberView object. In other words, it draws the line numbers. Its implementation is as follows:
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim graphics As Graphics = e.Graphics Dim textBrush As SolidBrush = New SolidBrush(ForeColor) Dim characterWidth As Integer = 8 Dim i, visibleLine As Integer For i = 1 To Math.Min(model.LineCount, VisibleLineCount) Dim number As Integer = i + controller.TopInvisibleLineCount Dim x As Integer = Me.Width - characterWidth - _ (number).ToString().Length * characterWidth Dim y As Integer = (i - 1) * (lineSpace + fontheight) graphics.DrawString(number.ToString(), Font, textBrush, x, y) Next i End Sub
The OnPaint method first obtains the Graphics object of the LineNumberView object from the PaintEventArgs argument:
Dim graphics As Graphics = e.Graphics
It then constructs a SolidBrush object that draws the line numbers:
Dim textBrush As SolidBrush = New SolidBrush(ForeColor)
Next, the OnPaint method iterates all the line numbers that need to be drawn in a For loop and uses the DrawString method of the Graphics class to draw the numbers:
For i = 1 To Math.Min(model.LineCount, VisibleLineCount) Dim number As Integer = i + controller.TopInvisibleLineCount Dim x As Integer = Me.Width - characterWidth - _ (number).ToString().Length * characterWidth Dim y As Integer = (i - 1) * (lineSpace + fontheight) graphics.DrawString(number.ToString(), Font, textBrush, x, y) Next i
The RedrawAll method invalidates the text area and forces a repaint. Its implementation is as follows:
Public Sub RedrawAll() Me.Invalidate() Me.Update() End Sub
The StyledTextArea class represents the controller in the MVC architecture of the StyledTextArea control. It is also the class that programmers see when they use the control. The other two main parts, the model and views, are private objects of this class. Therefore, they are not visible to the programmer who uses this control.
You declare the model and two views of the StyledTextArea control as private members of the StyledTextArea class:
Private view As View ' the view in the MVC pattern lineNumberView As LineNumberView Private model As Model ' the model in the MVC pattern
You can obtain and set the width of the LineNumberView through the public LineNumberWidth field:
Public LineNumberWidth As Integer = 50
There are also vertical and horizontal scrollbars that scroll the text area vertically and horizontally, respectively. Their widths are also fixed:
Private hScrollBar As New HScrollBar() Private vScrollBar As New VScrollBar() Const hScrollBarHeight As Integer = 15 Const vScrollBarWidth As Integer = 15
All parts of the user interface are packaged in a Panel control:
Private panel As New panel() ' we use panel so we can set the borderstyle
The sole reason for using a Panel is so that you can use its nice border. For details, see the "InitializeComponent" section.
As mentioned, the StyledTextArea class is the controller part in the MVC design pattern. Its main function is to take care of the user interaction with the user interface. The StyledTextArea control is a control that can receive the user's key input. A caret indicates the character insertion point visually in the View class. This caret is drawn at the location specified by the following caretLocation variable of type ColumnLine:
Private caretLocation As New ColumnLine(1, 1)
In addition, the StyledTextArea control also allows part of the text to be selected. The starting point and the ending point of the selected text are denoted by the following two variables. Their scope is Friend so that they can be accessed from the View class:
Friend selectionStartLocation As New ColumnLine(0, 0) Friend selectionEndLocation As New ColumnLine(0, 0)
A private Boolean (selecting) also internally indicates that the user is dragging the mouse to make a text selection. You use this variable in the view_MouseUp, view_MouseDown, and view_MouseMove event handlers:
Private selecting As Boolean ' user selecting text
Another Boolean (keyProcessed) indicates if a key press should be handled by the KeyPress event handler. See the view_KeyPress event handler and the ProcessDialogKey method in this class:
' indicates that the key press has been processed by ProcessDialogKey, ' so KeyPressed does not have to process this. Private keyProcessed As Boolean
Also, the edited private Boolean indicates if the text in the Model object has been edited—in other words, whether a character has been added or deleted:
' indicates whether the text has been edited Private editedField As Boolean
The last class variable in the StyledTextArea class is the TopInvisibleLineCount. This value has the Friend scope so that it can be accessed from both the View class and the LineNumberView class:
' represents the number of lines not displayed ' because the screen was scrolled down Friend TopInvisibleLineCount As Integer
The StyledTextArea class has one constructor that accepts no arguments. It consists only of the line that calls the InitializeComponent method:
Public Sub New() InitializeComponent() End Sub
The StyledTextArea class has several properties. Those properties with the scope Public are meant to be used by the user of the StyledTextArea control to set some settings of the control. The CaretColumnNo and CaretLineNo properties, which have the scope Friend, are meant to be used from the View class only.
The properties are explained in the following sections.
The CaretColor property represents the caret color in the View object. The implementation of this property is as follows:
Public Property CaretColor() As Color Get Return view.CaretColor End Get Set(ByVal caretColor As Color) view.CaretColor = caretColor End Set End Property
The CaretColumnNo property represents the caret's column location. You use it from inside the View class. Its implementation is as follows:
Friend Property CaretColumnNo() As Integer Get Return caretLocation.Column End Get Set(ByVal newColumnNo As Integer) If newColumnNo > 0 And newColumnNo <= GetCurrentLine().Length + 1 Then Dim oldColumnNo As Integer = caretLocation.Column caretLocation.Column = newColumnNo If oldColumnNo <> newColumnNo Then OnColumnChanged(New ColumnEventArgs(oldColumnNo, newColumnNo)) End If End If End Set End Property
The CaretLineNo property represents the caret's line location. You use it from inside the View class. Its implementation is as follows:
Friend Property CaretLineNo() As Integer Get Return caretLocation.Line End Get Set(ByVal newLineNo As Integer) If newLineNo > 0 And newLineNo <= model.LineCount Then Dim oldLineNo As Integer = caretLocation.Line caretLocation.Line = newLineNo If oldLineNo <> newLineNo Then OnLineChanged(New LineEventArgs(oldLineNo, newLineNo)) End If End If End Set End Property
The Edited property indicates whether a character has been inserted or deleted since the last time this property was reset. A program using the StyledTextArea control to display a document's content can reset this property whenever the document is reset. The next time the user tries to save the document, the program can check this property and skip the save if this property value is still False.
The Edited property implementation uses a field called editedField to store its value.
Its implementation is as follows:
Public Property Edited() As Boolean Get Return editedField End Get Set(ByVal value As Boolean) 'allows it to be reset/set, 'for example if the document using this control is saved editedField = value End Set End Property
The HighlightBackColor property represents the background color of the selected text:
Public Property HighlightBackColor() As Color Get Return view.highlightBackColor End Get Set(ByVal color As Color) view.highlightBackColor = color End Set End Property
The HighlightForeColor property represents the color of the selected text:
Public Property HighlightForeColor() As Color Get Return view.highlightForeColor End Get Set(ByVal color As Color) view.highlightForeColor = color End Set End Property
The LineCount property obtains the number of lines:
Public ReadOnly Property LineCount() As Integer Get Return model.LineCount End Get End Property
The LineNumberBackColor property represents the background color of the LineNumberView object:
Public Property LineNumberBackColor() As Color Get Return lineNumberView.BackColor End Get Set(ByVal color As Color) lineNumberView.BackColor = color End Set End Property
The LineNumberForeColor property represents the color used to draw line numbers:
Public Property LineNumberForeColor() As Color Get Return lineNumberView.ForeColor End Get Set(ByVal color As Color) lineNumberView.ForeColor = color End Set End Property
The read-only SelectedText property represents the user-selected text:
Public ReadOnly Property SelectedText() As String Get If HasSelection() Then Dim x1, y1, x2, y2 As Integer x1 = selectionStartLocation.Column y1 = selectionStartLocation.Line x2 = selectionEndLocation.Column y2 = selectionEndLocation.Line 'swap if necessary If y1 > y2 Or (y1 = y2 And x1 > x2) Then Dim t As Integer t = x1 : x1 = x2 : x2 = t t = y1 : y1 = y2 : y2 = t End If If y1 = y2 Then Return model.GetLine(y1).Substring(x1 - 1, x2 - x1) Else Dim sb As New StringBuilder(model.CharCount + 2 * model.LineCount) Dim lineCount As Integer = model.LineCount Dim i, lineNo As Integer For i = y1 To y2 Dim thisLine As String = model.GetLine(i) If i = y1 Then sb.Append(thisLine.Substring(x1 - 1)) ElseIf i = y2 Then sb.Append(Microsoft.VisualBasic.Constants.vbCrLf) sb.Append(thisLine.Substring(0, x2 - 1)) Else sb.Append(Microsoft.VisualBasic.Constants.vbCrLf) sb.Append(thisLine) End If Next Return sb.ToString() End If Else Return "" End If End Get End Property
The Text property represents all text in the StyledTextArea control. Setting this property overwrites the previous text. Its implementation is as follows:
Public Overrides Property Text() As String Get Dim sb As New StringBuilder(model.CharCount + 2 * model.LineCount) Dim lineCount As Integer = model.LineCount Dim i As Integer For i = 1 To lineCount Dim thisLine As String = model.GetLine(i) sb.Append(thisLine) If i < lineCount Then sb.Append(Microsoft.VisualBasic.Constants.vbCrLf) End If Next Return sb.ToString() End Get Set(ByVal s As String) If Not s Is Nothing Then Dim initialColumn As Integer = caretLocation.Column Dim initialLine As Integer = caretLocation.Line 'remove all lines Dim i As Integer Dim lineCount As Integer = model.LineCount For i = 2 To lineCount model.RemoveLine(2, False) Next model.SetLine(1, "") ' don't remove the first line caretLocation.Column = 1 caretLocation.Line = 1 caretLocation = model.InsertData(s, caretLocation) If caretLocation.Column <> initialColumn Then OnColumnChanged(New _ ColumnEventArgs(initialColumn, caretLocation.Column)) End If If caretLocation.Line <> initialLine Then OnLineChanged(New LineEventArgs(initialLine, caretLocation.Line)) End If ResetSelection() If Not view.IsCaretVisible() Then ScrollToShowCaret() End If RedrawAll() End If End Set End Property
The TextBackColor property represents the background color of the View object:
Public Property TextBackColor() As Color Get Return view.BackColor End Get Set(ByVal color As Color) view.BackColor = color End Set End Property
The TextForeColor property represents the color used to draw text in the View object:
Public Property TextForeColor() As Color Get Return view.ForeColor End Get Set(ByVal color As Color) view.ForeColor = color End Set End Property
The StyledTextArea class has the following methods.
The AdjustHScrollBar method adjusts the horizontal scrollbar to reflect the up-to- date values of the scrollbar's Maximum and Value properties. This method disables the scrollbar if the longest line is shorter than the screen width. It implementation is as follows:
Private Sub AdjustHScrollBar() If view.Width < model.LongestLineCharCount * view.characterWidth + 1 Then hScrollBar.Enabled = True hScrollBar.Maximum = model.LongestLineCharCount - _ view.VisibleCharCount + 1 '10 is the margin hScrollBar.Value = Math.Min(hScrollBar.Maximum, _ view.LeftInvisibleCharCount) Else hScrollBar.Enabled = False End If End Sub
The AdjustVScrollBar method adjusts the vertical scrollbar to reflect the current values of the scrollbar's Maximum and Value properties. This method disables the scrollbar if the number of lines is smaller than the number of lines the screen can display at a time. Its implementation is as follows:
Private Sub AdjustVScrollBar() 'adjust vScrollBar If view.VisibleLineCount < model.LineCount Then vScrollBar.Enabled = True vScrollBar.Maximum = model.LineCount - view.VisibleLineCount + 1 vScrollBar.Value = Math.Min(TopInvisibleLineCount, vScrollBar.Maximum) Else vScrollBar.Enabled = False End If End Sub
The Copy method copies the selected text into the Clipboard:
Public Sub Copy() 'copy selected text to clipboard If HasSelection() Then Clipboard.SetDataObject(SelectedText) End If End Sub
The Cut method copies the selected text into the Clipboard and deletes the selection:
Public Sub Cut() If HasSelection() Then Copy() RemoveSelection() ResetSelection() RedrawAll() End If End Sub
The Find method searches a pattern in the text and highlights the matching part of the text:
Public Function Find(ByVal userPattern As String, _ ByVal startColumn As Integer, ByVal startLine As Integer, _ ByVal caseSensitive As Boolean, _ ByVal wholeWord As Boolean, ByVal goUp As Boolean) As ColumnLine 'make sure startColumn and startLine is greater than 0 startColumn = Math.Max(startColumn, 1) Dim pattern As String = userPattern.Trim() Dim patternLength As Integer = pattern.Length Dim lineCount As Integer = model.LineCount Dim direction As Integer = 1 If goUp Then direction = -1 startLine = Math.Max(startLine, 1) startLine = Math.Min(startLine, lineCount) Dim lineNo As Integer = startLine If Not caseSensitive Then pattern = pattern.ToUpper() End If While lineNo <= lineCount And lineNo > 0 Dim thisLine As String = model.GetLine(lineNo) If Not caseSensitive Then thisLine = thisLine.ToUpper() End If Dim searchResult As Integer = -1 If lineNo = startLine Then If startColumn - 1 < thisLine.Length Then searchResult = thisLine.IndexOf(pattern, startColumn - 1) End If Else searchResult = thisLine.IndexOf(pattern) End If If searchResult <> -1 And wholeWord Then 'search successful but now test if the found pattern is a 'whole word by checking the characters after and before 'the match If searchResult > 0 Then 'test the character before the match If Char.IsLetterOrDigit( _ Convert.ToChar(thisLine.Substring(searchResult - 1, 1))) Then searchResult = -1 End If End If If searchResult <> -1 And _ thisLine.Length > searchResult + patternLength Then 'test the character after the match If Char.IsLetterOrDigit( _ Convert.ToChar(thisLine.Substring(searchResult + _ patternLength, 1))) Then searchResult = -1 End If End If End If If searchResult <> -1 Then 'successful 'move caret to new position CaretLineNo = lineNo CaretColumnNo = searchResult + patternLength + 1 Highlight(searchResult + 1, lineNo, _ searchResult + patternLength + 1, lineNo) RedrawAll() Return New ColumnLine(searchResult + 1, lineNo) End If lineNo = lineNo + direction End While Return New ColumnLine(0, 0) End Function
The GetCurrentLine method returns the string representing the line the caret is on:
Public Function GetCurrentLine() As String 'return the line where the caret is on Return model.GetLine(CaretLineNo) End Function
The GetLine method gets the line at the specified line number:
Public Function GetLine(ByVal lineNo As Integer) As String Return model.GetLine(lineNo) End Function
The HasSelection method indicates whether there is part of the text that is selected:
Friend Function HasSelection() As Boolean If selectionStartLocation.Equals(selectionEndLocation) Then Return False Else Return True End If End Function
The Highlight method highlights the selected text:
Private Sub Highlight(ByVal x1 As Integer, ByVal y1 As Integer, _ ByVal x2 As Integer, ByVal y2 As Integer) '(x1, y1) is the starting column,line of highlight '(x2, y2) is the end column,line of hightlight 'swap (x1,y1) and (x2,y2) if necessary If y1 > y2 Or (y1 = y2 And x1 > x2) Then Dim t As Integer t = x1 : x1 = x2 : x2 = t t = y1 : y1 = y2 : y2 = t End If selectionStartLocation.Column = x1 selectionStartLocation.Line = y1 selectionEndLocation.Column = x2 selectionEndLocation.Line = y2 End Sub
The hScrollBar_Scroll event handler handles the horizontal scrollbar's Scroll event. It adjusts the position of the horizontal scrollbar as well as scrolling the View object's screen, if necessary. Its implementation is as follows:
Private Sub hScrollBar_Scroll(ByVal sender As Object, _ ByVal e As ScrollEventArgs) Select Case e.Type Case ScrollEventType.SmallIncrement If view.LeftInvisibleCharCount < _ model.LongestLineCharCount - view.VisibleCharCount + 1 Then view.MoveScreen(hScrollBar.SmallChange) End If Case ScrollEventType.LargeIncrement If view.LeftInvisibleCharCount < _ model.LongestLineCharCount - view.VisibleCharCount Then Dim maxIncrement As Integer = _ Math.Min(hScrollBar.LargeChange, model.LongestLineCharCount - _ view.VisibleCharCount - view.LeftInvisibleCharCount) view.MoveScreen(maxIncrement) End If Case ScrollEventType.SmallDecrement view.MoveScreen(-hScrollBar.SmallChange) Case ScrollEventType.LargeDecrement view.MoveScreen(-hScrollBar.LargeChange) Case ScrollEventType.ThumbTrack view.LeftInvisibleCharCount = e.NewValue RedrawAll() Case ScrollEventType.ThumbPosition view.LeftInvisibleCharCount = e.NewValue RedrawAll() End Select End Sub
The IntializeComponent method initializes the controls and classes used in the StyledTextArea control. You call this method with the class's constructor. Its implementation is as follows:
Private Sub InitializeComponent() model = New Model() view = New View(model) view.controller = Me view.ForeColor = Color.Black view.BackColor = Color.White view.Cursor = Cursors.IBeam lineNumberView = New LineNumberView(model) lineNumberView.controller = Me lineNumberView.BackColor = Color.AntiqueWhite lineNumberView.ForeColor = Color.Black lineNumberView.TabStop = False vScrollBar.TabStop = False hScrollBar.TabStop = False ResizeComponents() panel.Dock = DockStyle.Fill panel.Controls.Add(lineNumberView) panel.Controls.Add(view) panel.Controls.Add(hScrollBar) panel.Controls.Add(vScrollBar) panel.BorderStyle = BorderStyle.Fixed3D Me.Controls.Add(panel) AddHandler view.KeyPress, AddressOf view_KeyPress AddHandler view.MouseDown, AddressOf view_MouseDown AddHandler view.MouseUp, AddressOf view_MouseUp AddHandler view.MouseMove, AddressOf view_MouseMove AddHandler model.LineCountChanged, AddressOf Model_LineCountChanged AddHandler model.LongestLineCharCountChanged, _ AddressOf model_LongestLineCharCountChanged AddHandler vScrollBar.Scroll, AddressOf vScrollBar_Scroll AddHandler hScrollBar.Scroll, AddressOf hScrollBar_Scroll AddHandler panel.Resize, AddressOf panel_Resize hScrollBar.Enabled = False hScrollBar.SmallChange = 1 ' 1 character hScrollBar.LargeChange = 2 vScrollBar.Enabled = False vScrollBar.SmallChange = 1 vScrollBar.LargeChange = 2 End Sub
The IsCaretOnFirstColumn method indicates whether the caret is on the first column of any line:
Private Function IsCaretOnFirstColumn() As Boolean Return (CaretColumnNo = 1) End Function
The IsCaretOnFirstLine method indicates whether the caret is on the first line:
Private Function IsCaretOnFirstLine() As Boolean Return (CaretLineNo = 1) End Function
The IsCaretOnFirstVisibleLine method indicates whether the caret is on the first visible line of the View object's text area:
Private Function IsCaretOnFirstVisibleLine() As Boolean Return (CaretLineNo = TopInvisibleLineCount + 1) End Function
The IsCaretOnLastColumn method indicates whether the caret is on the last column of the current line:
Private Function IsCaretOnLastColumn() As Boolean Return (CaretColumnNo = GetCurrentLine().Length + 1) End Function
The IsCaretOnLastLine method indicates whether the caret is on the last line:
Private Function IsCaretOnLastLine() As Boolean Return (CaretLineNo = model.LineCount) End Function
The IsCaretOnLastVisibleLine method indicates whether the caret is on the last visible line of the View object's text area:
Private Function IsCaretOnLastVisibleLine() As Boolean Return (CaretLineNo = view.VisibleLineCount + TopInvisibleLineCount) End Function
The IsInSelection method indicates whether the caret is in the selected text:
Public Function IsInSelection(ByVal column As Integer, _ ByVal line As Integer) As Boolean 'indicate that the character at (column, line) is selected If Not HasSelection() Then Return False Else Dim x1, y1, x2, y2 As Integer x1 = selectionStartLocation.Column y1 = selectionStartLocation.Line x2 = selectionEndLocation.Column y2 = selectionEndLocation.Line 'swap if necessary to make (x2, y2) below (x1, y1) If (y1 > y2) Or (y1 = y2 And x1 > x2) Then Dim t As Integer t = x2 : x2 = x1 : x1 = t t = y2 : y2 = y1 : y1 = t End If If y2 > model.LineCount Then y2 = model.LineCount If y1 = y2 And x1 > x2 Then Dim t As Integer t = x2 : x2 = x1 : x1 = t End If End If f line < y2 And line > y1 Then Return True ElseIf y1 = y2 And line = y1 And column >= x1 And column < x2 Then 'selection in one line Return True ElseIf line = y1 And line <> y2 And column >= x1 Then Return True ElseIf line = y2 And line <> y1 And column < x2 Then Return True Else Return False End If End If End Function
The model_LineCountChanged event handler handles the LineCountChanged event of the Model object. It adjusts the vertical scrollbar by calling the AdjustVScrollBar method:
Private Sub model_LineCountChanged(ByVal sender As Object, _ ByVal e As LineCountEventArgs) AdjustVScrollBar() End Sub
The model_LongestLineCharCountChanged event handler handles the LongestLineCharCountChanged event of the Model object. It checks if the number of characters the View object's text area can display is larger than the number characters of the longest line. If so, it sets the LeftInvisibleCharCount variable of the View object to zero. It then calls the AdjustHScrollBar method to adjust the horizontal scrollbar. The implementation of model_LongestLineCharCountChanged is as follows:
Private Sub model_LongestLineCharCountChanged(ByVal sender As Object, _ ByVal e As LongestLineEventArgs) If e.LongestLineCharCount < view.VisibleCharCount Then view.LeftInvisibleCharCount = 0 End If AdjustHScrollBar() End Sub
The OnColumnChanged method raises the ColumnChanged event:
Protected Overridable Sub OnColumnChanged(ByVal e As ColumnEventArgs) RaiseEvent ColumnChanged(Me, e) End Sub
The OnLineChanged method raises the LineChanged event:
Protected Overridable Sub OnLineChanged(ByVal e As LineEventArgs) RaiseEvent LineChanged(Me, e) End Sub
The panel_Resize event handler handles the Resize event of the Panel object by calling the ResizeComponents method:
Private Sub panel_Resize(ByVal sender As Object, ByVal e As EventArgs) ResizeComponents() End Sub
The Paste method pastes the content of the Clipboard starting from the caret location. If there is selected text when the Paste method is called, the selected text will be deleted. Only paste the content of the Clipboard if the format is Text. Its implementation is as follows:
Public Sub Paste() ' In this method, CaretLocation's Line and Column fields ' are accessed without going through the CaretLineNo and CaretColumnNo ' properties so as not to raise the OnLineChanged and OnColumnChanged ' events repeatedly. Dim buffer As IDataObject = Clipboard.GetDataObject() If buffer.GetDataPresent(DataFormats.Text) Then Dim initialColumn As Integer = caretLocation.Column Dim initialLine As Integer = caretLocation.Line If HasSelection() Then RemoveSelection() End If Dim s As String = buffer.GetData(DataFormats.Text).ToString() caretLocation = model.InsertData(s, caretLocation) If caretLocation.Column <> initialColumn Then OnColumnChanged(New ColumnEventArgs(initialColumn, caretLocation.Column)) End If If caretLocation.Line <> initialLine Then OnLineChanged(New LineEventArgs(initialLine, caretLocation.Line)) End If If HasSelection() Then ResetSelection() End If If Not view.IsCaretVisible() Then ScrollToShowCaret() End If RedrawAll() Else 'MsgBox("Incompatible data format") End If End Sub
The ProcessDialogKey method overrides the same method in the Control class to capture the pressing of any keyboard key. If the key corresponds to a character that needs to be displayed or inserted into the Model object, it resets the keyProcessed flag and in effect lets the view_KeyPress method handle the key press. Its implementation is as follows:
Protected Overrides Function ProcessDialogKey(ByVal keyData As Keys) As Boolean keyProcessed = True Select Case keyData Case Keys.Down ResetSelection() If Not IsCaretOnLastLine() Then If IsCaretOnLastVisibleLine() Then view.Scroll(1) End If CaretLineNo = CaretLineNo + 1 If CaretColumnNo > GetCurrentLine().Length + 1 Then CaretColumnNo = GetCurrentLine().Length + 1 End If ScrollToShowCaret() RedrawAll() End If Return True Case Keys.Up ResetSelection() If Not IsCaretOnFirstLine() Then If IsCaretOnFirstVisibleLine() Then view.Scroll(-1) End If CaretLineNo = CaretLineNo - 1 If CaretColumnNo > GetCurrentLine().Length + 1 Then CaretColumnNo = GetCurrentLine().Length + 1 End If ScrollToShowCaret() RedrawAll() End If Return True Case Keys.Right ResetSelection() If IsCaretOnLastColumn() Then If Not IsCaretOnLastLine() Then If IsCaretOnLastVisibleLine() Then view.Scroll(1) End If CaretLineNo = CaretLineNo + 1 CaretColumnNo = 1 End If Else CaretColumnNo = CaretColumnNo + 1 End If ScrollToShowCaret() RedrawAll() Return True Case Keys.Left ResetSelection() If IsCaretOnFirstColumn() Then If Not IsCaretOnFirstLine() Then If IsCaretOnFirstVisibleLine() Then view.Scroll(-1) End If CaretLineNo = CaretLineNo - 1 CaretColumnNo = GetCurrentLine().Length + 1 End If Else CaretColumnNo = CaretColumnNo - 1 End If ScrollToShowCaret() RedrawAll() Return True Case Keys.Delete 'Deleting character does not change caret position but 'may change the longest line char count If HasSelection() Then RemoveSelection() ResetSelection() ' then don't delete anything Else If CaretColumnNo = GetCurrentLine().Length + 1 Then ' at the end of line If CaretLineNo < model.LineCount Then 'concatenate current line and next line 'and delete next line Dim nextLine As String = model.GetLine(CaretLineNo + 1) model.SetLine(CaretLineNo, GetCurrentLine() & nextLine) model.RemoveLine(CaretLineNo + 1, True) End If Else 'delete one character model.DeleteChar(caretLocation) End If End If RedrawAll() Return True Case Else If CInt(Keys.Control And keyData) = 0 And _ CInt(Keys.Alt And keyData) = 0 Then ' let KeyPress process the key keyProcessed = False End If Return MyBase.ProcessDialogKey(keyData) End Select End Function
The RedrawAll method redraws the views of this component by calling the RedrawAll methods of the View class and the LineNumberView class and then adjusts the vertical and horizontal scrollbars:
Private Sub RedrawAll() view.RedrawAll() lineNumberView.RedrawAll() AdjustVScrollBar() AdjustHScrollBar() End Sub
The RemoveSelection method deletes the selected text:
Public Sub RemoveSelection() If Not HasSelection() Then Return End If Dim initialCaretLocation As ColumnLine = caretLocation ' after selection is removed, adjust CaretX position. Dim x1, y1, x2, y2 As Integer x1 = selectionStartLocation.Column y1 = selectionStartLocation.Line x2 = selectionEndLocation.Column y2 = selectionEndLocation.Line If y1 > y2 Or (y1 = y2 And x1 > x2) Then 'swap (x1, y1) and (x2, y2) Dim t As Integer t = x1 : x1 = x2 : x2 = t t = y1 : y1 = y2 : y2 = t End If If y1 = y2 Then Dim thisLine As String = model.GetLine(y1) model.SetLine(y1, thisLine.Substring(0, x1 - 1) & _ thisLine.Substring(x2 - 1, thisLine.Length - x2 + 1)) 'it's okay if event is raised when CaretColumnNo is set CaretColumnNo = x1 Else 'delete lines between y1 and y2 Dim j As Integer For j = 1 To (y2 - y1 - 1) model.RemoveLine(y1 + 1, False) 'false means "do not raise event" Next 'merge line y1 with line y2 and delete the original line y2 Dim thisLine As String = model.GetLine(y1) Dim nextLine As String = model.GetLine(y1 + 1) model.SetLine(y1, thisLine.Substring(0, x1 - 1) & _ nextLine.Substring(x2 - 1, nextLine.Length - x2 + 1)) model.RemoveLine(y1 + 1, True) ' CaretLineNo must be adjusted before CaretColumnNo because ' CaretColumnNo property will use CaretLineNo. Therefore, it ' is important that CaretLineNo contains the correct value CaretLineNo = y1 CaretColumnNo = x1 End If End Sub
The ResetSelection method resets the selectionStartLocation and selectionEndLocation variables by setting their Column and Line fields to zero:
Public Sub ResetSelection() selectionStartLocation.Column = 0 selectionStartLocation.Line = 0 selectionEndLocation.Column = 0 selectionEndLocation.Line = 0 End Sub
The ResizeComponents method resizes the components used in the StyledTextArea control. This method is called when the StyledTextArea control is first constructed and every time its size is changed:
Private Sub ResizeComponents() lineNumberView.Size = New Size(LineNumberWidth, panel.Height - _ hScrollBarHeight - 4) lineNumberView.Location = New Point(0, 0) view.Size = New Size(panel.Width - lineNumberView.Width - _ vScrollBarWidth - 4, panel.Height - hScrollBarHeight - 4) view.Location = New Point(lineNumberView.Width, 0) vScrollBar.Location = New Point(view.Width + lineNumberView.Width, 0) vScrollBar.Size = New Size(vScrollBarWidth, view.Height) hScrollBar.Location = New Point(0, view.Height) hScrollBar.Size = _ New Size(view.Width + lineNumberView.Width, hScrollBarHeight) AdjustVScrollBar() AdjustHScrollBar() End Sub
The ScrollToShowCaret method scrolls the View object horizontally and/or vertically to make sure the caret is visible:
Private Sub ScrollToShowCaret() If Not view.IsCaretVisible() Then If model.GetLine(CaretLineNo).Length > view.VisibleCharCount Then view.LeftInvisibleCharCount = GetCurrentLine().Length - _ view.VisibleCharCount Else view.LeftInvisibleCharCount = 0 End If If CaretLineNo > TopInvisibleLineCount + view.VisibleLineCount Then TopInvisibleLineCount = CaretLineNo - view.VisibleLineCount + 1 End If End If End Sub
The SelectAll method selects all the text in the Model object:
Public Sub SelectAll() selectionStartLocation.Column = 1 selectionStartLocation.Line = 1 selectionEndLocation.Column = model.GetLine(model.LineCount).Length + 1 selectionEndLocation.Line = model.LineCount RedrawAll() End Sub
The SelectLine method returns the String object on the specified line number:
Public Sub SelectLine(ByVal lineNo As Integer) If lineNo <= model.LineCount Then Dim thisLine As String = model.GetLine(lineNo) Dim length As Integer = thisLine.Length selectionStartLocation = New ColumnLine(1, lineNo) selectionEndLocation = New ColumnLine(length + 1, lineNo) caretLocation = selectionStartLocation RedrawAll() End If End Sub
The view_KeyPress event handler handles the key press of a key that corresponds to a character that is not processed by the ProcessDialogKey method:
Private Sub view_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs) If Not keyProcessed Then Dim c As Char = e.KeyChar Dim convertedChar As Integer = Convert.ToInt32(c) RemoveSelection() ResetSelection() Select Case convertedChar Case Keys.Back 'backspace If Not (IsCaretOnFirstColumn() And IsCaretOnFirstLine()) Then 'not at beginning of Model If IsCaretOnFirstColumn() Then Dim oldLine As String = model.GetLine(CaretLineNo) Dim prevLine As String = model.GetLine(CaretLineNo - 1) Dim newLine As String = prevLine & oldLine model.SetLine(CaretLineNo - 1, newLine) model.RemoveLine(CaretLineNo, True) CaretLineNo = CaretLineNo - 1 CaretColumnNo = prevLine.Length + 1 Else Dim oldLine As String = model.GetLine(CaretLineNo) Dim newLine As String = oldLine.Remove(CaretColumnNo - 2, 1) model.SetLine(CaretLineNo, newline) CaretColumnNo = CaretColumnNo - 1 End If editedField = True End If Case Keys.Return ' return key Dim oldLine As String = GetCurrentLine() Dim newLine As String = oldLine.Substring(0, CaretColumnNo - 1) Dim nextLine As String = oldLine.Substring(CaretColumnNo - 1) model.SetLine(CaretLineNo, newline) model.InsertLine(CaretLineNo + 1, nextLine) Dim needToScroll As Boolean = IsCaretOnLastVisibleLine() CaretColumnNo = 1 CaretLineNo = CaretLineNo + 1 If needToScroll Then view.Scroll(1) End If editedField = True Case Keys.Escape 'Escape 'escape key, do nothing Case Else model.InsertChar(c, caretLocation) CaretColumnNo = CaretColumnNo + 1 editedField = True End Select ScrollToShowCaret() RedrawAll() e.Handled = True End If End Sub
This view_MouseDown event handler handles the MouseDown event of the View object. A mouse down of the left button indicates the user is starting to select text in the View object's text area. It sets the selecting flag to True and sets selectionStartLocation and selectionEndLocation to the translated ColumnLine object of the mouse click's location. Its implementation is as follows:
Private Sub view_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) 'move the caret If e.Button = MouseButtons.Left Then view.RepositionCaret(e.X, e.Y) selecting = True Dim cl As ColumnLine = view.TranslateIntoCaretLocation(e.X, e.Y) selectionStartLocation = cl selectionEndLocation = cl RedrawAll() End If End Sub
The view_MouseMove event handler handles the MouseMove event of the View object. If the MouseMove event triggers when the value of selecting is True, it indicates the user is dragging the mouse to select text in the View object's text area. Its implementation is as follows:
Private Sub view_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) 'only respond when selecting, i.e. when the left button is pressed If selecting Then Dim cl As ColumnLine cl = view.TranslateIntoCaretLocation(e.X, e.Y) selectionEndLocation = cl RedrawAll() End If End Sub
The view_MouseUp event handler handles the MouseUp event of the View object. If the MouseUp event triggers when the selecting value is True, it indicates the end of the selection by the user. Its implementation is as follows:
Private Sub view_MouseUp(ByVal sender As Object, ByVal e As MouseEventArgs) 'move the caret If selecting And e.Button = MouseButtons.Left Then selecting = False 'reset selecting view.RepositionCaret(e.X, e.Y) RedrawAll() End If End Sub
The vScrollBar_Scroll event handler handles the Scroll event of the vertical scrollbar vScrollBar:
Private Sub vScrollBar_Scroll(ByVal sender As Object, _ ByVal e As ScrollEventArgs) Select Case e.Type Case ScrollEventType.SmallIncrement If TopInvisibleLineCount < model.LineCount - view.VisibleLineCount Then view.Scroll(vScrollBar.SmallChange) lineNumberView.RedrawAll() End If Case ScrollEventType.LargeIncrement If TopInvisibleLineCount < model.LineCount - view.VisibleLineCount Then Dim maxIncrement As Integer = _ Math.Min(vScrollBar.LargeChange, _ model.LineCount - view.VisibleLineCount - TopInvisibleLineCount) view.Scroll(maxIncrement) lineNumberView.RedrawAll() End If Case ScrollEventType.SmallDecrement view.Scroll(-vScrollBar.SmallChange) lineNumberView.RedrawAll() Case ScrollEventType.LargeDecrement view.Scroll(-vScrollBar.LargeChange) lineNumberView.RedrawAll() Case ScrollEventType.ThumbTrack TopInvisibleLineCount = e.NewValue RedrawAll() Case ScrollEventType.ThumbPosition TopInvisibleLineCount = e.NewValue RedrawAll() End Select End Sub
The StyledTextArea control has two events: ColumnChanged and LineChanged. With these events, the programmer who embeds the StyledTextArea control in a form will be notified when the user moves the caret. Programmers can then update the line and column position if they want. The following sections describe these events.
The ColumnChanged event raises every time the caret moves to another column:
Public Event ColumnChanged As ColumnEventHandler
The LineChanged event triggers every time the caret moves to another line:
Public Event LineChanged As LineEventHandler
If you are not using an IDE, you can compile the component by running the build.bat file in the Project directory. Here is the content of the file.
vbc /out:StyledTextArea.dll ColumnLine.vb LineNumberView.vb Model.vb StyledTextArea.vb Support.vb View.vb /t:library /r:System.dll,System.Windows.Forms.dll,System.Drawing.dll
The result will be a DLL file named StyledTextArea.dll.
Using the StyledTextArea control is no different from using other standard controls. You need to construct an object of the StyledTextArea class and then set the values of the properties you want to use. You can also handle one or both of its two events: ColumnChanged and LineChanged.
The Form1.vb file in the project's directory contains a form class (Form1) that uses a StyledTextArea control named textArea. The whole source code will not be reproduced here to save space. However, I will explain some key events and methods.
In addition to a StyledTextArea control, the Form1 class (displayed in Figure 1-7) contains the following standard controls:
A Label control (label1) to display the position of the caret in textArea.
A Button control (copyButton) to copy the selected text in textArea to the Clipboard.
A Button control (cutButton) to cut the selected text in textArea and copy it to the Clipboard.
A Button control (pasteButton) to paste the content of the Clipboard to textArea.
A Button control (selectButton) to select the text in textArea.
A TextBox control (findPattern) to receive text as a pattern to be searched in textArea.
A Button control (findButton) to conduct searching in textArea.
Three CheckBox controls (caseCB, wholeWordCB, and goUpCB) that receive parameters in the searching. You must select the caseCB check box to specify that the search should take case sensitivity into account. You must select the wholeWordCB check box to indicate that the find pattern is a whole word. Finally, the goUpCB check box indicates the reverse direction of the search.
Figure 1-7: Using the StyledTextArea control
The form has four private variables, declared as follows:
Private x1 As Integer = 1 Private y1 As Integer = 1 Private column As Integer = 1 Private line As Integer = 1
You use the variables x1 and y1 in the event handler that handles the Click event of findButton, and line and column represent the line and column position of the caret in textArea (respectively).
When the Form1 class is instantiated, its class's constructor calls the InitializeComponent method, which constructs all the controls it uses and sets the controls' properties. Of particular interest is the part of the method that instantiates the StyledTextArea control and sets its properties:
textArea = New StyledTextArea() textArea.Size = New Size(680, 345) textArea.Location = New Point(5, 5) textArea.CaretColor = Color.Red textArea.LineNumberBackColor = Color.FromArgb(240, 240, 240) textArea.LineNumberForeColor = Color.DarkCyan textArea.HighlightBackColor = Color.Black textArea.HighlightForeColor = Color.White textArea.TabIndex = 0
The Size, Location, and TabIndex properties inherit from the Control class, and the rest are the properties defined and implemented in the StyledTextArea class itself.
At the end of the InitializeComponent method, you call the UpdateLabel method:
Me.Controls.AddRange(New Control() _ {Label1, selectButton, pasteButton, cutButton, copyButton, goUpCB, _ textArea, findPattern, findButton, wholeWordCB, caseCB, findButton, _ findPattern}) UpdateLabel()
The UpdateLabel method updates the text of label1, displaying the current position of the caret in the StyledTextArea control:
label1.Text = "Ln: " & line & " Col: " & column
See that it simply uses the line and column variables? Initially, both line and column have the value of 1. Therefore, the Label control displays the following text:
Ln: 1 Col: 1
In this example, column gets updated every time the StyledTextArea control's caret moves to another column. You do this in the event handler textArea_ColumnChanged that handles the ColumnChanged event of the StyledTextArea control:
Private Sub textArea_ColumnChanged(ByVal sender As Object, _ ByVal e As ColumnEventArgs) Handles textArea.ColumnChanged column = e.NewColumn UpdateLabel() End Sub
The value of line is updated every time the StyledTextArea control's caret moves to another line. You do this in the event handler textArea_LineChanged that handles the LineChanged event of the StyledTextArea control:
Private Sub textArea_LineChanged(ByVal sender As Object, _ ByVal e As LineEventArgs) Handles textArea.LineChanged line = e.NewLine UpdateLabel() End Sub
The value of line is updated every time the StyledTextArea control's caret moves to another line. You do this in the event handler textArea_LineChanged that handles the LineChanged event of the StyledTextArea control:
Private Sub textArea_LineChanged(ByVal sender As Object, - ByVal e As LineEventArgs) Handles textArea.LineChanged line = e.NewLine UpdateLabel() End Sub
The copyButton_Click, cutButton_Click, pasteButton_Click, and selectButton_Click event handlers handle the Click events of copyButton, cutButton, pasteButton, and selectButton (respectively). These four controls' functions are self-explanatory. They simply use the Copy, Cut, Paste, and SelectAll methods of the StyledTextArea class. Note that they also call the StyledTextArea class's Focus method to return focus to textArea:
Private Sub copyButton_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles copyButton.Click textArea.Copy() textArea.Focus() End Sub Private Sub cutButton_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles cutButton.Click textArea.Cut() textArea.Focus() End Sub Private Sub pasteButton_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles pasteButton.Click textArea.Paste() textArea.Focus() End Sub Private Sub selectButton_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles selectButton.Click textArea.SelectAll() textArea.Focus() End Sub
Finally, the findButton_Click event handler handles the Click event of findButton. It uses the text entered into the findPattern TextBox control to search in textArea. It also uses the x1 and y1 variables to store the position of the start column and the start line for searching. In addition, it uses the values of the three CheckBox controls as parameters for the Find method of the StyledTextArea class.
To compile the application, you must first copy the StyledTextArea.dll file to the directory where the form file (form1.vb) resides. Then, change the directory to where form1.vb resides and type the following command:
vbc /t:winexe /r:System.dll,System.windows.forms.dll,System.Drawing.dll, StyledTextArea.dll form1.vb
To run the program, type the following:
form1
|