23.8. Client/Server Tic-Tac-Toe Using a Multithreaded Server We now present a networked version of the popular game Tic-Tac-Toe, implemented with stream sockets and client/server techniques. The program consists of a FrmTicTac-ToeServer application (Fig. 23.5) and a FrmTicTacToeClient application (Fig. 23.6). The FrmTicTacToeServer allows two FrmTicTacToeClients to connect to the server and play Tic-Tac-Toe against each other. Sample outputs appear in Fig. 23.6. When the server receives a client connection, lines 7079 of Fig. 23.5 create instances of class Player to process each client in a separate thread of execution. This enables the server to handle requests from both clients. The server assigns value "X" to the first client that connects (player "X" makes the first move), then assigns value "O" to the second client. Throughout the game, the server maintains information regarding the status of the board so that it can validate players' requested moves. However, neither the server nor the client can establish whether a player has won the gamein this application, method GameOver (lines 134137) always returns False. Each client maintains its own GUI version of the Tic-Tac-Toe board to display the game. The clients can place marks only in empty squares on the board. Class Square (Fig. 23.7) is used to define squares on the Tic-Tac-Toe board. Figure 23.5. Server side of client/server Tic-Tac-Toe program. 1 ' Fig. 23.5: FrmTicTacToeServer.vb 2 ' This class maintains a game of Tic-Tac-Toe for two 3 ' client applications. 4 Imports System.Net 5 Imports System.Net.Sockets 6 Imports System.Threading 7 Imports System.IO 8 9 Public Class FrmTicTacToeServer 10 Private board() As Byte ' the local representation of the game board 11 Private players() As Player ' two Player objects 12 Private playerThreads() As Thread ' Threads for client interaction 13 Private listener As TcpListener ' listen for client connection 14 Private currentPlayer As Integer ' keep track of whose turn it is 15 Private getPlayers As Thread ' Thread for acquiring client connections 16 Friend disconnected As Boolean = False ' True if the server closes 17 18 ' initialize variables and thread for receiving clients 19 Private Sub FrmTicTacToeServer_Load(ByVal sender As System.Object, _ 20 ByVal e As System.EventArgs) Handles MyBase .Load 21 22 board = New Byte(9) {} 23 players = New Player(2) {} 24 playerThreads = New Thread(2) {} 25 currentPlayer = 0 26 27 ' accept connections on a different thread 28 getPlayers = New Thread(New ThreadStart(AddressOf SetUp)) 29 getPlayers.Start() 30 End Sub ' FrmTicTacToeServer_Load 31 32 ' notify Players to stop Running 33 Private Sub FrmTicTacToeServer_FormClosing( _ 34 ByVal sender As System.Object, _ 35 ByVal e As System.Windows.Forms.FormClosingEventArgs) _ 36 Handles MyBase.FormClosing 37 38 disconnected = True 39 System.Environment.Exit(System.Environment.ExitCode) 40 End Sub ' FrmTicTacToeServer_FormClosing 41 42 ' Delegate that allows method DisplayMessage to be called 43 ' in the thread that creates and maintains the GUI 44 Private Delegate Sub DisplayDelegate(ByVal message As String) 45 46 ' method DisplayMessage sets txtDisplay's Text property 47 ' in a thread-safe manner 48 Friend Sub DisplayMessage(ByVal message As String) 49 ' if modifying txtDisplay is not thread safe 50 If txtDisplay.InvokeRequired Then 51 ' use inherited method Invoke to execute DisplayMessage 52 ' via a Delegate 53 Invoke(New DisplayDelegate(AddressOf DisplayMessage), _ 54 New Object() {message}) 55 ' OK to modify txtDisplay in current thread 56 Else 57 txtDisplay.Text &= message 58 End If 59 End Sub ' DisplayMessage 60 61 ' accepts connections from 2 players 62 Public Sub SetUp() 63 DisplayMessage("Waiting for players..." & vbCrLf) 64 65 ' set up Socket 66 listener = New TcpListener(IPAddress.Parse("localhost"), 50000) 67 listener.Start() 68 69 ' accept first player and start a player thread 70 players(0) = New Player(listener.AcceptSocket(), Me, 0) 71 playerThreads(0) = New Thread( _ 72 New ThreadStart(AddressOf players(0).Run)) 73 playerThreads(0).Start() 74 75 ' accept second player and start another player thread 76 players(1) = New Player(listener.AcceptSocket(), Me, 1) 77 playerThreads(1) = New Thread( _ 78 New ThreadStart(AddressOf players(1).Run)) 79 playerThreads(1).Start() 80 81 ' let the first player know that the other player has connected 82 SyncLock players(0) 83 players(0).threadSuspended = False 84 Monitor.Pulse(players(0)) 85 End SyncLock 86 End Sub ' SetUp 87 88 ' determine if a move is valid 89 Public Function ValidMove(ByVal location As Integer, _ 90 ByVal player As Integer) As Boolean 91 ' prevent another thread from making a move 92 SyncLock Me 93 ' while it is not the current player's turn, wait 94 While player <> currentPlayer 95 Monitor.Wait(Me) 96 End While 97 98 ' if the desired square is not occupied 99 If Not IsOccupied(location) Then 100 ' set the board to contain the current player's mark 101 If currentPlayer = 0 Then 102 board(location) = Convert.ToByte("X"c) 103 Else 104 board(location) = Convert.ToByte("O"c) 105 End If 106 107 ' set the currentPlayer to be the other player 108 currentPlayer = (currentPlayer + 1) Mod 2 109 110 ' notify the other player of the move 111 players(currentPlayer).OtherPlayerMoved(location) 112 113 ' alert the other player that it's time to move 114 Monitor.Pulse(Me) 115 Return True 116 Else 117 Return False 118 End If 119 End SyncLock 120 End Function ' ValidMove 121 122 ' determines whether the specified square is occupied 123 Public Function IsOccupied(ByVal location As Integer) As Boolean 124 If board(location) = Convert.ToByte("X"c) Or _ 125 board(location) = Convert.ToByte("O"c) Then 126 127 Return True 128 Else 129 Return False 130 End If 131 End Function ' IsOccupied 132 133 ' determines if the game is over 134 Public Function GameOver() As Boolean 135 ' place code here to test for a winner of the game 136 Return False 137 End Function ' GameOver 138 End Class ' FrmTicTacToeServer 139 140 ' class Player represents a tic-tac-toe player 141 Public Class Player 142 Friend connection As Socket ' Socket for accepting a connection 143 Private socketStream As NetworkStream ' network data stream 144 Private server As FrmTicTacToeServer ' reference to server 145 Private writer As BinaryWriter ' facilitates writing to the stream 146 Private reader As BinaryReader ' facilitates reading from the stream 147 Private number As Integer ' player number 148 Private mark As Char ' player's mark on the board 149 Friend threadSuspended As Boolean = True ' if waiting for other player 150 151 ' constructor requiring Socket, TicTacToeServerForm and Integer 152 ' objects as arguments 153 Public Sub New(ByVal socket As Socket, _ 154 ByVal serverValue As FrmTicTacToeServer, ByVal newNumber As Integer) 155 156 If newNumber = 0 Then 157 mark = "X"c 158 Else 159 mark = "O"c 160 End If 161 162 connection = socket 163 server = serverValue 164 number = newNumber 165 166 ' create NetworkStream object for Socket 167 socketStream = NewNetworkStream(connection) 168 169 ' create Streams for reading/writing bytes 170 writer = New BinaryWriter(socketStream) 171 reader = New BinaryReader(socketStream) 172 End Sub ' New 173 174 ' signal other player of move 175 Public Sub OtherPlayerMoved(ByVal location As Integer) 176 ' signal that opponent moved 177 writer.Write("Opponent moved.") 178 writer.Write(location) ' send location of move 179 End Sub ' OtherPlayerMoved 180 181 ' allows the players to make moves and receive moves 182 ' from the other player 183 Public Sub Run() 184 Dim done As Boolean = False 185 186 ' display on the server that a connection was made 187 If number = 0 Then 188 server.DisplayMessage("Player X connected" & vbCrLf) 189 Else 190 server.DisplayMessage("Player O connected" & vbCrLf) 191 End If 192 193 ' send the current player's mark to the client 194 writer.Write(mark) 195 196 ' if number equals 0 then this player is X, 197 ' otherwise O must wait for X's first move 198 If number = 0 Then 199 writer.Write("Player X connected.") 200 Else 201 writer.Write("Player O connected, please wait." & vbCrLf) 202 End If 203 204 ' X must wait for another player to arrive 205 If mark = "X"c Then 206 writer.Write("Waiting for another player.") 207 208 ' wait for notification from server that another 209 ' player has connected 210 SyncLock Me 211 While threadSuspended 212 Monitor.Wait(Me) 213 End While 214 End SyncLock 215 216 writer.Write("Other player connected. Your move.") 217 End If 218 219 ' play game 220 While Not done 221 ' wait for data to become available 222 While connection.Available = 0 223 Thread.Sleep(1000) 224 225 If server.disconnected Then 226 Return 227 End If 228 End While 229 230 ' receive data 231 Dim location As Integer = reader.ReadInt32() 232 233 ' if the move is valid, display the move on the 234 ' server and signal that the move is valid 235 If server.ValidMove(location, number) Then 236 server.DisplayMessage("loc: " & location & vbCrLf) 237 writer.Write("Valid move.") 238 ' signal that the move is invalid 239 Else 240 writer.Write("Invalid move, try again.") 241 End If 242 243 ' if game is over, set done to True to exit while loop 244 If server.GameOver() Then 245 done = True 246 End If 247 End While 248 249 ' close the socket connection 250 writer.Close() 251 reader.Close() 252 socketStream.Close() 253 connection.Close() 254 End Sub ' Run 255 End Class ' Player | Figure 23.6. Client side of client/server Tic-Tac-Toe program. 1 ' Fig. 23.6: FrmTicTacToeClient.vb 2 ' Client for the TicTacToe program. 3 Imports System.Drawing 4 Imports System.Net.Sockets 5 Imports System.Threading 6 Imports System.IO 7 8 Public Class FrmTicTacToeClient 9 Private board(,) As Square ' local representation of the game board 10 Private currentSquareValue As Square ' Square that this player chose 11 Private outputThread As Thread ' Thread for receiving data from server 12 Private connection As TcpClient ' client to establish connection 13 Private stream As NetworkStream ' network data stream 14 Private writer As BinaryWriter ' facilitates writing to the stream 15 Private reader As BinaryReader ' facilitates reading from the stream 16 Private myMark As Char ' player's mark on the board 17 Private myTurn As Boolean ' is it this player's turn? 18 Private brush As SolidBrush ' brush for drawing X's and O's 19 Private done As Boolean = False ' True when game is over 20 21 ' initialize variables and thread for connecting to server 22 Private Sub FrmTicTacToeClient_Load(ByVal sender As System.Object, _ 23 ByVal e As System.EventArgs) Handles MyBase .Load 24 25 board = New Square(3, 3 ) {} 26 27 ' create 9 Square objects and place them on the board 28 board(0 , 0) = New Square(pnlBoard0, " "c , 0 ) 29 board(0, 1) = New Square(pnlBoard1, " "c , 1 ) 30 board(0, 2) = New Square(pnlBoard2, " "c , 2 ) 31 board(1, 0) = New Square(pnlBoard3, " "c , 3 ) 32 board(1, 1) = New Square(pnlBoard4, " "c , 4 ) 33 board(1, 2) = New Square(pnlBoard5, " "c , 5 ) 34 board(2, 0) = New Square(pnlBoard6, " "c , 6 ) 35 board(2, 1) = New Square(pnlBoard7, " "c , 7 ) 36 board(2, 2) = New Square(pnlBoard8, " "c , 8 ) 37 38 ' create a SolidBrush for writing on the Squares 39 brush = New SolidBrush(Color.Black ) 40 41 ' make connection to server and get the associated 42 ' network stream 43 connection = New TcpClient( "localhost" , 50000) 44 stream = connection.GetStream() 45 writer = New BinaryWriter(stream) 46 reader = New BinaryReader(stream) 47 48 ' start a new thread for sending and receiving messages 49 outputThread = New Thread(New ThreadStart( AddressOf Run)) 50 outputThread.Start() 51 End Sub ' FrmTicTacToeClient_Load 52 53 ' repaint the Squares 54 Private Sub FrmTicTacToeClient_Paint( ByVal sender As System.Object, _ 55 ByVal e As System.Windows.Forms.PaintEventArgs) _ 56 Handles MyBase .Paint 57 58 PaintSquares() 59 End Sub ' FrmTicTacToeClient_Paint 60 61 ' game is over 62 Private Sub FrmTicTacToeClient_FormClosing( _ 63 ByVal sender As System.Object, _ 64 ByVal e As System.Windows.Forms.FormClosingEventArgs) _ 65 Handles MyBase.FormClosing 66 67 done = True 68 System.Environment.Exit(System.Environment.ExitCode) 69 End Sub ' FrmTicTacToeClient_FormClosing 70 71 ' Delegate that allows method DisplayMessage to be called 72 ' in the thread that creates and maintains the GUI 73 Private Delegate Sub DisplayDelegate(ByVal message As String) 74 75 ' method DisplayMessage sets txtDisplay's Text property 76 ' in a thread-safe manner 77 Private Sub DisplayMessage(ByVal message As String) 78 ' if modifying txtDisplay is not thread safe 79 If txtDisplay.InvokeRequired Then 80 ' use inherited method Invoke to execute DisplayMessage 81 ' via a Delegate 82 Invoke(New DisplayDelegate(AddressOf DisplayMessage), _ 83 New Object () {message}) 84 ' OK to modify txtDisplay in current thread 85 Else 86 txtDisplay.Text &= message 87 End If 88 End Sub ' DisplayMessage 89 90 ' Delegate that allows method ChangeIdLabel to be called 91 ' in the thread that creates and maintains the GUI 92 Private Delegate Sub ChangeIdLabelDelegate(ByVal message As String) 93 94 ' method ChangeIdLabel sets txtDisplay's Text property 95 ' in a thread-safe manner 96 Private Sub ChangeIdLabel(ByVal label As String) 97 ' if modifying lblId is not thread safe 98 If lblId.InvokeRequired Then 99 ' use inherited method Invoke to execute ChangeIdLabel 100 ' via a Delegate 101 Invoke(New ChangeIdLabelDelegate(AddressOf ChangeIdLabel), _ 102 New Object () {label}) 103 ' OK to modify lblId in current thread 104 Else 105 lblId.Text = label 106 End If 107 End Sub ' ChangeIdLabel 108 109 ' draws the mark of each square 110 Public Sub PaintSquares() 111 Dim g As Graphics 112 113 ' draw the appropriate mark on each panel 114 For row As Integer = 0 To 2 115 116 For column As Integer = 0 To 2 117 ' get the Graphics for each Panel 118 g = board(row, column).SquarePanel.CreateGraphics() 119 120 ' draw the appropriate letter on the panel 121 g.DrawString(board(row, column).Mark.ToString(), _ 122 pnlBoard0.Font, brush, 10, 8) 123 Next column 124 125 Next row 126 End Sub ' PaintSquares 127 128 ' send location of the clicked square to server 129 Private Sub square_MouseUp(ByVal sender As System.Object, _ 130 ByVal e As System.Windows.Forms.MouseEventArgs) _ 131 Handles pnlBoard0.MouseUp, pnlBoard5.MouseUp, pnlBoard4.MouseUp, _ 132 pnlBoard3.MouseUp, pnlBoard2.MouseUp, pnlBoard1.MouseUp, _ 133 pnlBoard8.MouseUp, pnlBoard7.MouseUp, pnlBoard6.MouseUp 134 135 ' for each square check if that square was clicked 136 For row As Integer = 0 To 2 137 138 For column As Integer = 0 To 2 139 If board(row, column).SquarePanel.Equals(sender) Then 140 currentSquareValue = board(row, column) 141 142 ' send the move to the server 143 SendClickedSquare(board(row, column).Location) 144 End If 145 Next column 146 Next row 147 End Sub ' square_MouseUp 148 149 ' control thread that allows continuous update of the 150 ' TextBox display 151 Public Sub Run() 152 myMark = reader.ReadChar() ' first get players's mark (X or O) 153 ChangeIdLabel("You are player """ & myMark & """") 154 155 If myMark = "X"c Then 156 myTurn = True 157 Else 158 myTurn = False 159 End If 160 161 ' process incom ing messages 162 Try 163 ' receive messages sent to client 164 While Not done 165 ProcessMessage(reader.ReadString()) 166 End While 167 Catch ex As IOException 168 MessageBox.Show("Server is down, game over", "Error", _ 169 MessageBoxButtons.OK, MessageBoxIcon.Error ) 170 End Try 171 End Sub ' Run 172 173 ' process messages sent to client 174 Public Sub ProcessMessage(ByVal message As String) 175 ' if the move the player sent to the server is valid 176 ' update the display, set that square's mark to be 177 ' the mark of the current player and repaint the board 178 If message = "Valid move." Then 179 DisplayMessage("Valid move, please wait." & vbCrLf ) 180 currentSquareValue.Mark = myMark 181 PaintSquares() 182 ElseIf message = "Invalid move, try again." Then 183 ' if the move is invalid, display that and it is now 184 ' this player's turn again 185 DisplayMessage(message & vbCrLf ) 186 myTurn = True 187 ElseIf message = "Opponent moved." Then 188 ' if opponent moved, find location of their move 189 Dim location As Integer = reader.ReadInt32() 190 191 ' set that square to have the opponents mark and 192 ' repaint the board 193 If myMark = "X"c Then 194 board(location \ 3 , location Mod 3 ).Mark = "O"c 195 Else 196 board(location \ 3 , location Mod 3 ).Mark = "X"c 197 End If 198 PaintSquares() 199 200 DisplayMessage("Opponent moved. Your turn." & vbCrLf ) 201 202 ' it is now this player's turn 203 myTurn = True 204 Else 205 DisplayMessage(message & vbCrLf ) ' display message 206 End If 207 End Sub ' ProcessMessage 208 209 ' sends the server the number of the clicked square 210 Public Sub SendClickedSquare(ByVal location As Integer ) 211 ' if it is the current player's move right now 212 If myTurn Then 213 ' send the location of the move to the server 214 writer.Write(location) 215 216 ' it is now the other player's turn 217 myTurn = False 218 End If 219 End Sub ' SendClickedSquare 220 221 ' write-only property for the current square 222 Public WriteOnly Property CurrentSquare() As Square 223 Set(ByVal value As Square) 224 currentSquareValue = value 225 End Set 226 End Property ' CurrentSquare 227 End Class ' FrmTicTacToeClient
At the start of the game. (a)
(b)
Af ter Player X makes the first move. (c)
(d)
After Player O makes the second move. (e)
(f)
After Player X makes the final move. (g)
(h)
The Tie Tac Toe Server's output from the client interactions. (i) | Figure 23.7. Class Square. 1 ' Fig. 23.7: Square.vb 2 ' A Square on the TicTacToe board. 3 4 ' the representation of a square in a tic-tac-toe grid 5 Public Class Square 6 Private panel As Panel ' GUI Panel that represents this Square 7 Private markValue As Char ' player's markValue on this Square (if any) 8 9 ' locationValue on the board of this Square 10 Private locationValue As Integer 11 12 ' constructor 13 Public Sub New(ByVal newPanel As Panel, ByVal newMark As Char, _ 14 ByVal newLocation As Integer) 15 16 panel = newPanel 17 markValue = newMark 18 locationValue = newLocation 19 End Sub ' New 20 21 ' property SquarePanel; the panel which the square represents 22 Public ReadOnly Property SquarePanel() As Panel 23 Get 24 Return panel 25 End Get 26 End Property ' SquarePanel 27 28 ' property Mark; the markValue on the square 29 Public Property Mark() As Char 30 Get 31 Return markValue 32 End Get 33 Set(ByVal value As Char) 34 markValue = value 35 End Set 36 End Property ' Mark 37 38 ' property Location; the square's locationValue on the board 39 Public ReadOnly Property Location() As Integer 40 Get 41 Return locationValue 42 End Get 43 End Property ' Location 44 End Class ' Square | FrmTicTacToeServer Class FrmTicTacToeServer (Fig. 23.5) uses its Load event handler (lines 1930) to create a Byte array to store the moves the players have made (line 22). The program creates an array of two references to Player objects (line 23) and an array of two references to Thread objects (line 24). Each element in both arrays corresponds to a Tic-Tac-Toe player. Variable currentPlayer is set to 0 (line 25), which corresponds to player "X". In our program, player "X" makes the first move. Lines 2829 create and start Thread getPlayers, which the FrmTicTacToeServer uses to accept connections so that the current Thread does not block while awaiting players. Lines 4459 define DisplayDelegate and DisplayMessage, allowing any thread to modify txtdisplay's Text property. This time, the DisplayMessage method is declared as Friend, so it can be called inside a method of class Player through a FrmTicTac-ToeServer reference. Classes in the same project can access each other's Friend members. Thread getPlayers executes method SetUp (lines 6286), which creates a TcpListener object to listen for requests on port 50000 (lines 6667). This object then listens for connection requests from the first and second players. Lines 70 and 76 instantiate Player objects representing the players, and lines 7173 and 7779 create two Threads that execute the Run methods of each Player object. The Player constructor (Fig. 23.5, lines 153172) receives as arguments a reference to the Socket object (i.e., the connection to the client), a reference to the FrmTicTac-ToeServer object and an Integer indicating the player number (from which the constructor infers the mark "X" or "O" used by that player). FrmTicTacToeServer calls method Run (lines 183254) after instantiating a Player object. Lines 187194 notify the server of a successful connection and send the client the Char that the client will place on the board when making a move. If Run is executing for Player "X", lines 205217 execute, causing Player "X" to wait for a second player to connect. Lines 211213 suspend the Player "X" Thread until the server signals that Player "O" has connected. The server notifies the Player of the connection by setting the Player's tHReadSuspended variable to False (line 83). When threadSuspended becomes False, Player exits the loop in lines 211213. Lines 220247 in method Run enable the user to play the game. Each iteration of this statement waits for the client to send an Integer specifying where on the board to place the "X" or "O"the Player then places the mark on the board if the specified mark location is valid (i.e., if that location does not already contain a mark). Note that the loop continues execution only if Boolean variable done is False. This variable is set to true by event handler FrmTicTacToeServer_FormClosing of class FrmTicTacToeServer, which is invoked when the server closes the connection. Line 222 of Fig. 23.5 begins a loop that iterates until Socket property Available indicates that there is information to receive from the Socket (or until the server disconnects from the client). If there is none, the THRead sleeps for one second. On awakening, the THRead uses property Disconnected to check whether server variable disconnected is true (line 225). If so, the Thread exits the method (thus terminating the Thread); otherwise, the Thread loops again. However, if property Available indicates that there is data to receive, the loop in lines 222228 terminates, enabling the information to be processed. This information contains an Integer representing the location in which the client wants to place a mark. Line 231 calls method ReadInt32 of the BinaryReader object (which reads from the NetworkStream created with the Socket) to read this Integer. Line 235 then passes the Integer to FrmTicTacToeServer method ValidMove. If this method validates the move, the Player places the mark in the desired location. Method ValidMove (lines 89120) sends the client a message indicating whether the move was valid. Locations on the board correspond to numbers from 0 to 8 (02 for the top row, 35 for the middle and 68 for the bottom). All the statements in the method are enclosed in a SyncLock statement so that only one move can be attempted at a time. This prevents two players from modifying the game's state information simultaneously. If the Player attempting to validate a move is not the current player (i.e., the one allowed to make a move), that Player is placed in a Wait state until its turn. If the user attempts to place a mark on a location that already contains a mark, method ValidMove returns False. However, if the user has selected an unoccupied location (line 99), lines 101105 place the mark on the local representation of the board. Line 111 notifies the other Player that a move has been made, and line 114 invokes the Pulse method so that the waiting Player can validate a move. The method then returns TRue to indicate that the move is valid. When FrmTicTacToeClient (Fig. 23.6) executes, it creates a TextBox to display messages from the server and the Tic-Tac-Toe board representation. The board is created out of nine Square objects (Fig. 23.7) that contain Panels on which the user can click, indicating the position on the board in which to place a mark. FrmTicTacToeClient's Load event handler (lines 2251) opens a connection to the server (line 43) and obtains a reference to the connection's associated NetworkStream object from TcpClient (line 44). Lines 4950 start a thread to read messages sent from the server to the client. The server passes messages (e.g., whether each move is valid) to method ProcessMessage (lines 174207). If the message indicates that a move is valid (line 178), the client sets its Mark to the current square (the square that the user clicked) and repaints the board. If the message indicates that a move is invalid (line 182), the client notifies the user to click a different square. If the message indicates that the opponent made a move (line 187), line 189 reads an Integer from the server specifying where on the board the client should place the opponent's Mark. FrmTicTacToeClient includes a Delegate/method pair for allowing threads to modify lblId's Text property (lines 92107), as well as DisplayDelegate and DisplayMesage for modifying txtdisplay's Text property (lines 7388). |