Implementing the Project


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.

click to expand
Figure 1-5: The StyledTextArea component

Viewing the Class Diagram

Figure 1-6 shows the class diagram for the StyledTextArea component.

click to expand
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.

Using the ColumnLine Structure

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

start example
 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 
end example

Declaring the Delegates

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

start example
 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) 
end example

I give you more details on these delegates when discussing the Model and StyledTextArea classes.

Using the Event Arguments 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.

ColumnEventArgs

Listing 1-7 shows the ColumnEventArgs class.

Listing 1-7: ColumnEventArgs

start example
 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 
end example

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.

LineEventArgs

Listing 1-8 shows the LineEventArgs class.

Listing 1-8: LineEventArgs

start example
 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 
end example

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.

LineCountEventArgs

Listing 1-9 shows the LineCountEventArgs class.

Listing 1-9: LineCountEventArgs

start example
 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 
end example

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.

LongestLineEventArgs

Listing 1-10 shows the LongestLineEventArgs class.

Listing 1-10: LongestLineEventArgs

start example
 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 
end example

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.

Understanding the Model Class

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 

Constructing the Model Object

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.

Understanding the Model Class's Properties

The Model class has three public properties: CharCount, LineCount, and LongestLineCharCount.

CharCount

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

start example
 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 
end example

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.

LineCount

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 

LongestLineCharCount

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

start example
 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 
end example

The getter of the LongestLineCharCount property works by checking the length of each string in the ArrayList.

Understanding the Model Class's Methods

The Model class has several methods you can use to manipulate the data in the Model object.

DeleteChar

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

start example
 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 
end example

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 

GetLine

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 

InsertChar

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

start example
 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 
end example

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 

InsertData

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

start example
 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 
end example

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) 

InsertLine

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

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

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

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

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

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

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 

Understanding the Model Class's Events

The Model class contains several events.

LineCountChanged

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

LongestLineCharCountChanged raises when the number of characters of the longest line changes:

 Public Event LongestLineCharCountChanged As LongestLineEventHandler 

Understanding the View Class

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.

Constructing a View 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 

Understanding the View Class's Properties

The following sections discuss each property in the View class.

CaretColor

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.

VisibleCharCount

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 

VisibleLineCount

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.

Understanding the View Class's Methods

The View class has the following methods.

DisplayCaret

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.

Dispose

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 

DrawCaret

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) 

GetCaretXAbsolutePosition

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.

GetFontHeight

The GetFontHeight method returns the value of the FontHeight property:

 Public Function GetFontHeight() As Integer   Return FontHeight End Function 

GetStringWidth

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.

IsCaretVisible

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.

MoveScreen

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 

OnPaint

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) 

PaintSelectionArea

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.

RedrawAll

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 

RepositionCaret

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.

Scroll

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.

TranslateIntoCaretLocation

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 

Using the LineNumberView Class

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.

Constructing a LineNumberView Object

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 

Understanding the LineNumberView Class's Property

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 

Understanding the LineNumberView Class's Methods

The LineNumberView class has two methods: OnPaint and RedrawAll. The following sections explain both.

OnPaint

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 

RedrawAll

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 

Using the StyledTextArea Class

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 

Constructing the StyledTextArea Class

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 

Understanding the StyledTextArea Class's Properties

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.

CaretColor

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 

CaretColumnNo

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 

CaretLineNo

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 

Edited

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 

HighlightBackColor

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 

HighlightForeColor

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 

LineCount

The LineCount property obtains the number of lines:

 Public ReadOnly Property LineCount() As Integer   Get     Return model.LineCount   End Get End Property 

LineNumberBackColor

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 

LineNumberForeColor

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 

SelectedText

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 

Text

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 

TextBackColor

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 

TextForeColor

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 

Understanding the StyledTextArea Class's Methods

The StyledTextArea class has the following methods.

AdjustHScrollBar

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 

AdjustVScrollBar

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 

Copy

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 

Cut

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 

Find

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 

GetCurrentLine

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 

GetLine

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 

HasSelection

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 

Highlight

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 

hScrollBar_Scroll

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 

InitializeComponent

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 

IsCaretOnFirstColumn

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 

IsCaretOnFirstLine

The IsCaretOnFirstLine method indicates whether the caret is on the first line:

 Private Function IsCaretOnFirstLine() As Boolean   Return (CaretLineNo = 1) End Function 

IsCaretOnFirstVisibleLine

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 

IsCaretOnLastColumn

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 

IsCaretOnLastLine

The IsCaretOnLastLine method indicates whether the caret is on the last line:

 Private Function IsCaretOnLastLine() As Boolean   Return (CaretLineNo = model.LineCount) End Function 

IsCaretOnLastVisibleLine

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 

IsInSelection

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 

model_LineCountChanged

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 

model_LongestLineCharCountChanged

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 

OnColumnChanged

The OnColumnChanged method raises the ColumnChanged event:

 Protected Overridable Sub OnColumnChanged(ByVal e As ColumnEventArgs)   RaiseEvent ColumnChanged(Me, e) End Sub 

OnLineChanged

The OnLineChanged method raises the LineChanged event:

 Protected Overridable Sub OnLineChanged(ByVal e As LineEventArgs)   RaiseEvent LineChanged(Me, e) End Sub 

panel_Resize

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 

Paste

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 

ProcessDialogKey

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 

RedrawAll

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 

RemoveSelection

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 

ResetSelection

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 

ResizeComponents

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 

ScrollToShowCaret

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 

SelectAll

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 

SelectLine

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 

view_KeyPress

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 

view_MouseDown

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 

view_MouseMove

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 

view_MouseUp

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 

vScrollBar_Scroll

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 

Understanding the StyledTextArea Class's Events

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.

ColumnChanged

The ColumnChanged event raises every time the caret moves to another column:

 Public Event ColumnChanged As ColumnEventHandler 

LineChanged

The LineChanged event triggers every time the caret moves to another line:

 Public Event LineChanged As LineEventHandler 

Compiling the Component

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

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.

click to expand
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.

Compiling and Running the Application

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 




Real World. NET Applications
Real-World .NET Applications
ISBN: 1590590821
EAN: 2147483647
Year: 2005
Pages: 82

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net