Building a Group Chat App with JavaFX — Multithread Socket Programming in Java
The goal with this project was to create a simple Group Chat Application that uses TCP (Transmission Control Protocol).
What is TCP?
TCP, in simple words, is a protocol very commonly used within digital network communications and ensures end-to-end data delivery. TCP organizes data so that it can be transmitted between a server and a client. (See More)
In this project I had two Java files, Client.java and Server.java. The server takes one parameter (a port number) and Client takes two parameters (an IP address, a port number).
java Server 3000java Client localhost 3000
What was I trying to accomplish?
The end goal was to make an application where clients can connect exchange messages within a server with all the clients connected to that server. A few important things about the app:
1) A client has to be able to enter an username before the Group Chat App is displayed.
2) A client should be disconnected from the server if the client types “bye”.
3) A client should me in READ MODE ONLY if the client doesn’t enter an username.
4) If a client enters “All Users”, the Server needs to send all active Clients to that client.
5) The architecture is server-client. Therefore, all the messages from the clients need to go to the server, which then distributes the message to all the clients.
End Product Screenshot (5 clients exchanging messages through the same server):
Alright, let’s get to the code!
IDE of choice: IntelliJ.
Starting of with our Client.java.
When you first make an JavaFX application with IntelliJ, this is what is given to you as a starting point.
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{}
public static void main(String[] args) {
launch(args);
}
}
First step was get to the address and the port number given to the program through the command line args, set up my stage and my scene.
Here I got the command line arguments by calling super, I created a scene to my JavaFX app by creating an instance of Scene, and I set that scene to my stage. I gave my scene a style sheet called “main.css” so I could do some css styling later, and I displayed our stage by callying stage.show() in the end of start().
public class Main extends Application {
Stage window;
String address;
int port; @Override
public void start(Stage primaryStage) throws Exception{ address = super.getParameters().getRaw().get(0);
port = Integer.parseInt(super.getParameters().getRaw().get(1)); //Window
this.window = primaryStage;
this.window.setTitle("Client"); //Scene
Scene scene = new Scene(600, 520);
scene.getStylesheets().add("css/main.css");
//Finish Window
this.window.setScene(scene);
this.window.show();}
public static void main(String[] args) {
launch(args);
}
}
The next step was to set up my Grid Pane and create an instance of a Client, an inner class of Main that that I crated in the next step. Grid Pane is what I used to do all of the Scene layout. (See More).
In this step I created an instance of Grid Panel called grid, and I set up columns and rows that I layer used to display the components I created. In this app I used TWO columns and THREE rows. First Column was set to occupy 80% of the grid, Second Column 20%. First Row I set to occupy 75% of the grid, Second Row 20% and Third Row 20%. (Wait, 115%? Not really, grid pane is smart enough to make some computation behind the scene and size each row accordingly to the percentages of each other).
public class Main extends Application {
Stage window;
String address;
int port;@Override
public void start(Stage primaryStage) throws Exception{address = super.getParameters().getRaw().get(0);
port = Integer.parseInt(super.getParameters().getRaw().get(1)); //Window
this.window = primaryStage;
this.window.setTitle("Client"); //Grid
GridPane grid = new GridPane();
grid.setVgap(8);
grid.setHgap(10);
grid.setPadding(new Insets(10, 10, 10, 10)); //set up columns
ColumnConstraints column0 = new ColumnConstraints();
column0.setPercentWidth(80);
ColumnConstraints column1 = new ColumnConstraints();
column1.setPercentWidth(20);
//set up rows
RowConstraints row0 = new RowConstraints();
row0.setPercentHeight(75);
RowConstraints row1 = new RowConstraints();
row1.setPercentHeight(20);
RowConstraints row2 = new RowConstraints();
row1.setPercentHeight(20);
//add columns and rows to the grid
grid.getRowConstraints().addAll(row0, row1, row2);
grid.getColumnConstraints().addAll(column0, column1); //Scene
Scene scene = new Scene(grid, 600, 520);
scene.getStylesheets().add("css/main.css"); //Client Stuff
new Client(address, port, grid); //Finish Window
this.window.setScene(scene);
this.window.show();}
public static void main(String[] args) {
launch(args);
}
}
Now it’s time to start writing our Client Class. Our client will need a few properties, some of the will be used to do the network communication I was trying to accomplish and some of them will be used to display my program to the UI.
For the network communication, I needed a Socket, a DataInputStream, a DataOutputStream, a list of messages sent to that Client and a list of messages typed by that Client
Socket: Serves it as an endpoint in a network connection that is useful to receive and send data.
DataInputStream: Read data packets sent by the end points within that network connection.
DataOutputStream: Write data packets to the end points within that network connection.
LinkedBlockingQueue: Very useful to avoid busy waiting in infinite loops, since .take() just sits and waits until an object is available to be taken.
public static class Client{
//Network Communication
private Socket socket = null;
private DataInputStream in = null;
private DataOutputStream out = null;
private final LinkedBlockingQueue<String> MESSAGES;
private final LinkedBlockingQueue<String> MESSAGES_BY_CLIENT;
private String username = ""; //UI Stuff
private final TextFlow CHAT = new TextFlow();
private final TextArea CHAT_INPUT = new TextArea();
private final Button BUTTON_SEND = new Button("SEND"); public Client(String address, int port, GridPane grid){ this.MESSAGES_BY_CLIENT = new LinkedBlockingQueue<>();
this.MESSAGES = new LinkedBlockingQueue<>(); try {
socket = new Socket(address, port);
in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
out = new DataOutputStream(socket.getOutputStream());
BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); } catch (Exception e) {
System.out.println("error " + e.getMessage());
}}
Next step was to have Client to read a message from the Server.java that asks the Client for it’s username. Read client’s input, set the username of that Client class to the username entered, and write that username back to the Server. After that, the Client reads a ‘Welcome Message’ from the Server and adds it to its chat.
public static class Client{
//Network Communication
private Socket socket = null;
private DataInputStream in = null;
private DataOutputStream out = null;
private final LinkedBlockingQueue<String> MESSAGES;
private final LinkedBlockingQueue<String> MESSAGES_BY_CLIENT;
private String username = ""; //UI Stuff
private final TextFlow CHAT = new TextFlow();
private final TextArea CHAT_INPUT = new TextArea();
private final Button BUTTON_SEND = new Button("SEND"); public Client(String address, int port, GridPane grid){ this.MESSAGES_BY_CLIENT = new LinkedBlockingQueue<>();
this.MESSAGES = new LinkedBlockingQueue<>();
try {
socket = new Socket(address, port);
in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
out = new DataOutputStream(socket.getOutputStream());
BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); //Server sents "Enter Username: " message, Client reads it
String serverMessage = in.readUTF();
System.out.println(serverMessage);
//Client inputs username
username = input.readLine();
setUsername(username);
out.writeUTF(username);
//connection accepted and welcome
serverMessage = in.readUTF();
Text text = new Text(serverMessage);
CHAT.getChildren().add(text); }catch (Exception e) {
System.out.println("error " + e.getMessage());
} }
}
And then a bunch of UI components design that I won’t be getting to much into it. But all that I’m doing here, It’s organizing what I want where in the UI grid, and making it responsive, positioning it vertically, horizontally, etc.
One the components was a button to send message, and I had to overwrite its event handler when clicked. I overwrote with a function that takes the message from the chat input, adds to my LinkedBlockingQueu e MESSAGES_BY_CLIENT and erases the chat input. Pretty standard logic.
public static class Client{
//Network Communication
private Socket socket = null;
private DataInputStream in = null;
private DataOutputStream out = null;
private final LinkedBlockingQueue<String> MESSAGES;
private final LinkedBlockingQueue<String> MESSAGES_BY_CLIENT;
private String username = ""; //UI Stuff
private final TextFlow CHAT = new TextFlow();
private final TextArea CHAT_INPUT = new TextArea();
private final Button BUTTON_SEND = new Button("SEND"); public Client(String address, int port, GridPane grid){ this.MESSAGES_BY_CLIENT = new LinkedBlockingQueue<>();
this.MESSAGES = new LinkedBlockingQueue<>();
try {
socket = new Socket(address, port);
in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
out = new DataOutputStream(socket.getOutputStream());
BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); //Server sents "Enter Username: " message, Client reads it
String serverMessage = in.readUTF();
System.out.println(serverMessage);
//Client inputs username
username = input.readLine();
setUsername(username);
out.writeUTF(username);
//connection accepted and welcome
serverMessage = in.readUTF();
Text text = new Text(serverMessage);
CHAT.getChildren().add(text); //set up chat to UI
CHAT.getStyleClass().add("server-chat");
CHAT.setPadding(new Insets(5, 5, 5, 5));
GridPane.setHgrow(CHAT, Priority.ALWAYS);
GridPane.setVgrow(CHAT, Priority.ALWAYS);
ScrollPane sp = new ScrollPane();
sp.setContent(CHAT);
GridPane.setConstraints(sp, 0, 0);
//Client Status (Circle)
Circle statusCircle = new Circle(0, 0, 10);
statusCircle.getStyleClass().add("status-circle");
GridPane.setValignment(statusCircle, VPos.TOP);
GridPane.setHgrow(statusCircle, Priority.ALWAYS);
GridPane.setVgrow(statusCircle, Priority.ALWAYS);
GridPane.setConstraints(statusCircle, 1, 0);
//Client Status (Label)
Label statusLabel = new Label("Online");
statusLabel.getStyleClass().add("status-label");
GridPane.setValignment(statusLabel, VPos.TOP);
GridPane.setHalignment(statusLabel, HPos.CENTER);
GridPane.setHgrow(statusLabel, Priority.ALWAYS);
GridPane.setVgrow(statusLabel, Priority.ALWAYS);
GridPane.setConstraints(statusLabel,1, 0 );
//UserName
Label usernameLabel = new Label("User: " + username);
usernameLabel.getStyleClass().add("status-username");
GridPane.setValignment(usernameLabel, VPos.TOP);
GridPane.setHalignment(usernameLabel, HPos.LEFT);
GridPane.setHgrow(usernameLabel, Priority.ALWAYS);
GridPane.setVgrow(usernameLabel, Priority.ALWAYS);
usernameLabel.setPadding(new Insets(30, 0, 0, 0));
GridPane.setConstraints(usernameLabel,1, 0 );
//set up user input area to UI
CHAT_INPUT.getStyleClass().add("chat-input");
CHAT_INPUT.setPromptText("Enter message...");
GridPane.setHgrow(CHAT_INPUT, Priority.ALWAYS);
GridPane.setVgrow(CHAT_INPUT, Priority.ALWAYS);
GridPane.setConstraints(CHAT_INPUT, 0, 1);
//set button to send message to UI
BUTTON_SEND.getStyleClass().add("button-send");
GridPane.setHgrow(BUTTON_SEND, Priority.ALWAYS);
GridPane.setVgrow(BUTTON_SEND, Priority.ALWAYS);
GridPane.setHalignment(BUTTON_SEND, HPos.RIGHT);
GridPane.setValignment(BUTTON_SEND, VPos.TOP);
GridPane.setConstraints(BUTTON_SEND, 0, 2);
grid.getChildren().addAll(sp, CHAT_INPUT, BUTTON_SEND, statusCircle, statusLabel, usernameLabel);
//set up BUTTON to send message to UI
setUpButtonSend(); }catch (Exception e) {
System.out.println("error " + e.getMessage());
} } public void setUsername(String username){
this.username = username;
} public void setUpButtonSend(){
BUTTON_SEND.setOnAction(e -> {
String message = CHAT_INPUT.getText();
MESSAGES_BY_CLIENT.add(message);
CHAT_INPUT.setText("");
});
}
}
Now Lets Get Started with …(drum roll)… the Multithreading!
The First Thread I did was a Thread that would handle user input and runs until user’s input equals ‘bye’.
Remember the button handler clicked event that I overwrote? That’s what this thread is based of. It keeps listening to the LinkedBlockingQueue MESSAGES_BY_CLIENT and if there are any new messages there, it reads that messages and writes to the Server. If that message is “bye”, the client connection is closed.
Note: This code is also inside the Client Constructor.
Thread userInput = new Thread(){
public void run(){
try {
String message = "";
while (!message.equals("bye")) {
try {
message = MESSAGES_BY_CLIENT.take();
out.writeUTF(message);
} catch (IOException | InterruptedException e) {
System.out.println(e);
}
}
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
in.close();
out.close();
socket.close();
System.exit(0);
}
catch (IOException e){
System.out.println("Error here " + e.getMessage());
}
}
};
userInput.start();
The second Thread I did was a thread that kept reading messages to the Client if any messages was available in the LinkedBloquingQueue MESSAGES. But where the messages were being input in MESSAGES? Well, that’s what our third and final Thread was responsible for.
Note: This code is also inside the Client Constructor.
Thread readMessagesToClient = new Thread(){
public void run(){
String message = "";
while(true){
try{
message = MESSAGES.take();
addMessageToChat(message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
readMessagesToClient.start();
The third and final Thread for our Client.java does exactly what I just told you. It keeps listening to the Server if any messages are being sent, if any message is sent from the server to the Client, the Client grabs that message and puts in the LinkedBloquingQueue MESSAGES.
This DataInputStream and DataOutputStream had to be unique for each client-server connection, therefore I put this Thread in another inner Class of Main Called “ReadMessagesFromServer” that implemented Runnable.
Note: This ran this thread in the Client Constructor as well by doing:
ReadMessagesFromServer server = new ReadMessagesFromServer(socket);
new Thread(server).start();
And this is our third and final Thread for Client.java:
private class ReadMessagesFromServer implements Runnable{
DataInputStream in = null;
DataOutputStream out = null;
Socket socket;
ReadMessagesFromServer(Socket socket){
this.socket = socket;
}
public void run(){
try{
in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
out = new DataOutputStream(socket.getOutputStream());
while(true){
try{
String line = in.readUTF();
MESSAGES.put(line);
}catch (IOException | InterruptedException e){
// e.printStackTrace();
}
}
}catch(IOException e){
System.out.println(e.getMessage());
}
}
}
Perfect, last go over the Server.java now!
Some basic set up we already went over on Client.java that is very similar to Server.java
public class Main extends Application {
Stage window;
@Override
public void start(Stage primaryStage){
//Window
this.window = primaryStage;
this.window.setTitle("Server");
//Grid
GridPane grid = new GridPane();
grid.setVgap(8);
grid.setHgap(10);
grid.setPadding(new Insets(10, 10, 10, 10));
//Scene
Scene scene = new Scene(grid, 1080, 720);
scene.getStylesheets().add("css/main.css");
//Server Stuff
new Server(3000, grid);
//Finish Window
this.window.setScene(scene);
this.window.show();
}
public static class Server {
//Socket
private Socket socket = null;
private ServerSocket server = null;
} public static void main(String[] args) {
launch(args);
}
}
Again, the Server Class will need some properties for UI purposes and Network Communication purposes.
For Network Communication purposes I created an ArrayList that kept track of the CLIENTS connected to the Server and a LinkedBlockingQueue named MESSAGES for the same reason of the one in the Client Side.
For UI purposes I create a ListView to display the Clients’ usernames Connected to the Server, an ArrayList with different colors to pick a random color to use in the background of client’s username and a SimpleDateFormat that formattes the current data in hours:minutes:seconds.
public class Main extends Application {
Stage window;
int port;@Override
public void start(Stage primaryStage){
port = Integer.parseInt(super.getParameters().getRaw().get(0)); //Window
this.window = primaryStage;
this.window.setTitle("Server");
//Grid
GridPane grid = new GridPane();
grid.setVgap(8);
grid.setHgap(10);
grid.setPadding(new Insets(10, 10, 10, 10));
//Scene
Scene scene = new Scene(grid, 1080, 720);
scene.getStylesheets().add("css/main.css");
//Server Stuff
new Server(port, grid);
//Finish Window
this.window.setScene(scene);
this.window.show();
}
public static class Server {
//Socket
private Socket socket = null;
private ServerSocket server = null; //Network Communication
private final ArrayList<HandleClient> CLIENTS = new ArrayList<>();
private final LinkedBlockingQueue<String> messages;
//UI
private final ListView<String> CLIENTS_LIST_VIEW = new ListView<>();
private final TextArea CHAT = new TextArea();
ArrayList<String> clientColors = new ArrayList<String> ();
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
public Server(int port, GridPane grid) {
this.messages = new LinkedBlockingQueue<>();
//Customize CLIENTS_LIST_VIEW
customizeClientsListView();
clientColors.add("#FCBA03");
clientColors.add("#03FCBA");
clientColors.add("#BA03FC");
clientColors.add("#FC03C1");
clientColors.add("#03FC3D");
} public static void main(String[] args) {
launch(args);
}
}
SWEET, now that we got that done, let’s start with the THREADS!!!
First Thread I did was one to keep accepting clients, and create a ClientHandler Thread for each one of them. So each one is handled in a different Thread and we can have multiple Client connections in the Server. Oh very important to note that we’re keeping track of all these HandleClient threads created in the ArrayList<HandleClient> CLIENTS.
There is also some UI designing here to get some messages showing up in the UI Chat, you should look up for Grid Pane if you’re interesting in this kind of stuff.
Note: This code is inside the Server Constructor.
Thread acceptClients = new Thread() {
public void run() {
try {
server = new ServerSocket(port);
CHAT.setEditable(false);
CHAT.appendText("Server Started\n");
CHAT.appendText("Waiting Client...");
GridPane.setConstraints(CHAT,0,0);
CHAT.getStyleClass().add("server-CHAT");
GridPane.setConstraints(CLIENTS_LIST_VIEW,1,0);
CLIENTS_LIST_VIEW.getStyleClass().add("clients-list");
grid.getChildren().addAll(CHAT, CLIENTS_LIST_VIEW);
while (true) {
socket = server.accept();
//handle multithreading for clients
HandleClient client = new HandleClient(socket.getRemoteSocketAddress().toString(), socket.getPort(), socket);
CLIENTS.add(client);
new Thread(client).start();
}
} catch (Exception e) {
System.out.println("Error here " + e.getMessage());
}
}
};acceptClients.start();
Now, ok I’m making a Thread for each client inside a HandleClient class, but what is HandleClient doing? Next step…
This code is very similar with the way we handled Client-Server connecting in Client.java, but with a few differences, or if makes it easier to understand, just the other way around it.
At first we just send a message to our Client side asking for the username, and while Client doesn’t enter that username, we just don’t do anything. If the username entered by the Client is a blank, that means that username is in READ ONLY MODE and we will deal with its messages later.
Once we get the username for the Client, we just set the username for this class so we can access the username later, and we add that client to the ListView Client List in the UI by calling addClientToUI(username).
After that, connection is accepted and just like we did for client, and for now on
all the messages that needs to be sent to the clients, will be put in the LinkedBlockingQueue messages. We start doing that with a first “Welcome” + username message.
private class HandleClient implements Runnable{
private DataInputStream in = null;
private DataOutputStream out = null;
public String address;
public int port;
public Socket socket;
private String username;
public HandleClient(String address, int port, Socket socket){
this.address = address;
this.port = port;
this.socket = socket;
try{
this.in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
this.out = new DataOutputStream(socket.getOutputStream());
} catch (Exception e) {
System.out.println("Error here " + e.getMessage());
}
}
public void run(){
try {
//get username from client
String username = null;
out.writeUTF("Enter username: ");
while(username == null){
username = in.readUTF();
setUsername(username);
addClientToUI(username);
}
if (this.username.isBlank()){
out.writeUTF("Connection Accepted" + "\nWelcome " + username +
"\nNOTE: SINCE YOU HAVEN'T PROVIDED AN USERNAME, YOU'RE IN MODE READ ONLY.");
}
else{
out.writeUTF("Connection Accepted" + "\nWelcome " + username);
//welcome message to server
String message = "Welcome " + this.username;
try{
messages.put(message);
}catch (InterruptedException e){
System.out.println(e.getMessage());
}
addMessageToChat(message);
}}
catch (IOException e){
System.out.println("Error :" + e.getMessage());
}
}
public void setUsername(String username){
this.username = username;
}
public String getUsername(){
return this.username;
}}
Following up this code, still inside our try method inside public void run() inside our HandleClient Nested Class, we need to handle the messages that the client is writing, and again.. we will do that until the message is “bye”. All this messages will be added to the LinkedBlockingQueue mesages as explained previously.
If message is “bye” we sent a last goodbye message, sleep for two seconds (attempt to fix race condition for socket closed exceptions) and we close the connection. We also remove the Client from the Client List View in the UI by calling removeClientFromUI().
try {
.... String line = "";
while (!line.equals("bye")) {
try {
line = this.in.readUTF();
String line_formatted = this.username + ": " + line;
if (!this.username.isBlank()){
messages.put(line_formatted);
//print to server
addMessageToChat(line_formatted);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
System.out.println(this.username);
}
}
try{
String goodbyeMessage = "Server: Goodbye " + this.username + ", see ya soon!";
messages.put(goodbyeMessage);
addMessageToChat(goodbyeMessage);
}
catch (InterruptedException e){
System.out.println(e.getMessage());
}
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
removeClientToUI(this.getUsername());
CLIENTS.remove(this);
this.in.close();
this.out.close();
socket.close(); }catch (IOException e){
System.out.println("Error :" + e.getMessage());
}}public void removeClientFromUI(String username){
Platform.runLater(new Runnable() {
public void run(){
CLIENTS_LIST_VIEW.getItems().remove(username);
}
});
}
One thing important to note is that concurrent/parallel threads can not change the UI, so for that reason we have to wrap our UI components changes with Platform.runLater(). That’s how my methods addClientToUI, removeClientToUI, and addMessageToChat() were coded:
public void addMessageToChat(String message){
Platform.runLater(new Runnable() {
public void run(){
Date date = new Date(System.currentTimeMillis());
String dateFormatted = formatter.format(date);
CHAT.appendText("\n" + dateFormatted + " " + message);
}
});
}
public void addClientToUI(String username){
Platform.runLater(new Runnable() {
public void run(){
CLIENTS_LIST_VIEW.getItems().add(username);
}
});
}
public void removeClientFromUI(String username){
Platform.runLater(new Runnable() {
public void run(){
CLIENTS_LIST_VIEW.getItems().remove(username);
}
});
}
Well so by now, we’re getting all those messages from the Clients and storing those messages in a LinkedBlockingQueue named ‘messages’, so now all that we need to do is to HAVE ANOTHER THREAD(that’s right!) to keep track of ‘messages’ and send it to all the Clients.
Inside the Server Constructor I wrote another thread and started it just below the acceptclients Thread and that’s how the Server Constructor looks like it now.
public Server(int port, GridPane grid) {
this.messages = new LinkedBlockingQueue<>();
//Customize CLIENTS_LIST_VIEW
customizeClientsListView();
clientColors.add("#FCBA03");
clientColors.add("#03FCBA");
clientColors.add("#BA03FC");
clientColors.add("#FC03C1");
clientColors.add("#03FC3D");
Thread acceptClients = new Thread() {
public void run() {
try {
server = new ServerSocket(port);
CHAT.setEditable(false);
CHAT.appendText("Server Started\n");
CHAT.appendText("Waiting Client...");
GridPane.setConstraints(CHAT,0,0);
CHAT.getStyleClass().add("server-CHAT");
GridPane.setConstraints(CLIENTS_LIST_VIEW,1,0);
CLIENTS_LIST_VIEW.getStyleClass().add("clients-list");
grid.getChildren().addAll(CHAT, CLIENTS_LIST_VIEW);
while (true) {
socket = server.accept();
//handle multithreading for clients
HandleClient client = new HandleClient(socket.getRemoteSocketAddress().toString(), socket.getPort(), socket);
CLIENTS.add(client);
new Thread(client).start();
}
} catch (Exception e) {
System.out.println("Error here " + e.getMessage());
}
}
};
acceptClients.start();
Thread writeMessages = new Thread() {
public void run() {
while (true) {
try {
String message = messages.take();
for (HandleClient client : CLIENTS) {
client.write("> " + message);
}
} catch (Exception e) {
System.out.println("Error is it here " + e.getMessage());
}
}
}
};
writeMessages.start();
}
One thing is missing right? HandleClient doesn’t have a method write… let’s fix that
public void write(String message){
try{
this.out.writeUTF(message);
}catch (IOException e){
System.out.println("Error here " + e.getMessage());
}
}
And that’s it, just like that we’re done. Oh wait, wondering how I changed the background color of the clients cells in the Clients List View? That’s how..
To be very honest I’m not 100% sure how this overwriting is really happening, but that’s just how the documentation in docs.oracle.com tells you to do it. I just add the random color feature on top of it. It’s easy to get the idea, it’s just the callback functions that kind of get messy in my head.
public void customizeClientsListView(){
CLIENTS_LIST_VIEW.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
@Override
public ListCell<String> call(ListView<String> param){
return new ListCell<String>(){
@Override
public void updateItem(String item, boolean empty){
super.updateItem(item, empty);
if (item == null || empty){
setText(null);
setStyle("-fx-background-color: white");
}
else{
setText(item);
String color = getRandomColor();
setStyle("-fx-background-color: " + color);
}
}
};
}
});
}public String getRandomColor(){
Random rand = new Random();
return clientColors.get(rand.nextInt(clientColors.size()));
}
I did forget to explain how I solved the allUsers feature in the app, so I’ll do it right now.
Inside the Handle Client run() method, I added a if statement under the while(!line.equals(“bye) which just checks if the line equals(“allUsers”), if so write.Clients.toString().. After that, all I had to do was to overwrite the toString() method in the HandleClient class, and all that does is return the HandleClient username.
Code updated…
String line = "";
while (!line.equals("bye")) {
try {
line = this.in.readUTF();
String line_formatted = this.username + ": " + line;
if (line.equals("allUsers")){
write(CLIENTS.toString());
addMessageToChat(line_formatted);
continue;
}
else if (!this.username.isBlank()){
messages.put(line_formatted);
//print to server
addMessageToChat(line_formatted);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
System.out.println(this.username);
}
}@Override
public String toString(){
return getUsername();
}
And that’s it… some screenshots to prove functionality: