23. diel - Java chat - Klient - Dokončenie 1. časť
V minulej lekcii, Java chat - Server - Chat plugin , sme vytvorili chat plugin pre server. V prvej časti dnešného Java tutoriálu zobrazíme prihlásených užívateľov v GUI. V druhej časti vytvoríme widgety reprezentujúci konverzáciu a jej obsah.
Zobrazenie prihlásených užívateľov
Užívateľa budeme zobrazovať v ListView, ktoré sa nachádza
v súbore main.fxml a jeho referencie je uložená v kontroleru
MainController. Najskôr toto ListView natypujeme na
triedu ChatContact:
@FXML
private ListView<ChatContact> lvContactList;
Ďalej vytvoríme v MainController u novú inštančný
konštantu typu IChatService a hneď ju vytvoríme inštanciu:
private final IChatService chatService = new ChatService(communicator);
V metóde handleConnect() nastavíme
ConnectController u chatService.
controller.setChatService(chatService);
Ďalej vyplníme telo metódy initialize().
@Override public void initialize(URL url, ResourceBundle resourceBundle) { lvContactList.setCellFactory(param -> new ChatEntryCell()); chatService.getClients().addListener(this.chatClientListener); }
V metóde zatiaľ robíme dve veci:
- nastavíme továreň záznamov pre
ListView - pridáme poslucháča klientov v
chatService
Teraz vytvoríme konštantu, ktorá bude obsahovať anonymné funkciu, ktorá
sa bude spúšťať pri zmene klientov v chatService:
private final MapChangeListener <<? super String, ? super ChatContact> chatClientListener = change -> { if (change.wasAdded()) { lvContactList.getItems().addAll(change.getValueAdded()); } if (change.wasRemoved()) { lvContactList.getItems().removeAll(change.getValueRemoved()); } };
ChatEntryCell
V balíčku widget založíme novú triedu
ChatEntryCell, ktorá bude reprezentovať jeden záznam v
ListView:
package cz.stechy.chat.widget; public class ChatEntryCell extends ListCell <ChatContact> { private final Circle circle = new Circle(); private final Label lblName = new Label(); private final Region spacer = new Region(); private final Label lblUnreadedMessages = new Label(); private final HBox container = new HBox(circle, lblName, spacer, lblUnreadedMessages); { circle.setRadius(15); HBox.setHgrow(spacer, Priority.ALWAYS); container.setAlignment(Pos.CENTER_LEFT); container.setSpacing(8); } private void bind(ChatContact item) { circle.fillProperty().bind(item.contactColorProperty()); lblName.textProperty().bind(item.nameProperty()); lblUnreadedMessages.textProperty().bind(item.unreadedMessagesProperty().asString()); lblUnreadedMessages.visibleProperty().bind(item.unreadedMessagesProperty().greaterThan(0)); } private void unbind() { circle.fillProperty().unbind(); lblName.textProperty().unbind(); lblUnreadedMessages.textProperty().unbind(); } @Override protected void updateItem(ChatContact item, boolean empty) { super.updateItem(item, empty); setText(null); if (empty) { unbind(); setGraphic(null); } else { bind(item); setGraphic(container); } } }
Každý záznam bude obsahovať koliesko s náhodne vygenerovanú farbou, ďalej meno používateľa a vpravo sa bude zobrazovať počet neprečítaných správ od daného užívateľa.

ChatTabContent
Jednotlivé správy budú reprezentované triedou
ChatTabContent. Tieto správy majú definované vlastné view v
súboroch:
/fxml/chat/message_incomming.fxml/fxml/chat/message_outcomming.fxml
Jediný rozdiel v súboroch je rozmiestnenie prvkov, inak sú totožné. Každá správa bude obsahovať koliesko reprezentujúci užívateľa, meno používateľa a samotný obsah správy.
V balíčku widget založíme novú triedu
ChatTabContent:
package cz.stechy.chat.widget; public class ChatTabContent { @FXML private Circle circle; @FXML private Label lblFrom; @FXML private TextArea areaMessage; @FXML private ImageView imgLoading; private void enableArea() { imgLoading.setVisible(false); areaMessage.setDisable(false); } void setColor(Color color) { circle.setFill(color); } void setContactName(String name) { lblFrom.setText(name); } void setMessage(String message) { areaMessage.setText(message); } void askForResizeTextArea() { if (areaMessage.getLength() <= 58) { enableArea(); return; } CompletableFuture.runAsync(() -> { try { Thread.sleep(1000); } catch (InterruptedException ignored) {} }, ThreadPool.COMMON_EXECUTOR) .thenAcceptAsync(ignored -> { final Node text = areaMessage.lookup(".text"); if (text == null) { return; } areaMessage.prefHeightProperty().bind(Bindings.createDoubleBinding( () -> text.getBoundsInLocal().getHeight(), text.boundsInLocalProperty()).add(20)); enableArea(); }, ThreadPool.JAVAFX_EXECUTOR); } }
Obsah správy budeme zobrazovať s oneskorením, aby sa veľkosť
TextArea nastavila správne a my nemuseli používať
ScrollBar pre prečítanie jednotlivé správy. Oneskorenie je
opäť vyriešené za pomocou CompletableFuture, kde na začiatku
jednoducho počkáme jednu sekundu (v pracovnom vlákne). Po uplynutí tejto
doby vyhľadáme v TextArea metódou lookup()
text a podľa dĺžky textu nastavíme výšku. Nakoniec schováme
obrázok s načítací animáciou.
Chattáb
Teraz sa dostávame k samotnému chatovacímu oknu. V
MainController u máme TabPane, v ktorom budeme
zobrazovať jednotlivé konverzácie. Každá konverzácia bude v jednom tabu. V
balíčku widget založíme novú triedu ChatTab:
public class ChatTab extends Tab{}
Najskôr vytvoríme konštanty, do ktorých uložíme cesty k dôležitým súborom.
private static final URL PATH_CONTENT_INCOMING = ChatTab.class.getResource("/fxml/chat/chat_tab_content_incoming.fxml"); private static final URL PATH_CONTENT_OUTCOMING = ChatTab.class.getResource("/fxml/chat/chat_tab_content_outcoming.fxml"); private static final String PATH_IMG_TYPING = ChatTab.class.getResource("/img/typing.gif").toExternalForm(); private static final String PATH_IMG_LOADING = ChatTab.class.getResource("/img/loading.gif").toExternalForm();
Obrázky pre načítanie a indikátor písania sú priložené v zdrojových na konci článku.
Ďalej vytvoríme inštančné konštanty:
private final ScrollPane container = new ScrollPane(); private final VBox messagesContiainer = new VBox(); private final ImageView imgTyping = new ImageView(new Image(PATH_IMG_TYPING)); private final StackPane imageContainer = new StackPane(); private final Circle circle = new Circle(); private final ChatContact chatContact;
Každý tab bude obsahovať ScrollPane s VBox em.
Do VBox u sa budú vkladať jednotlivé správy, teda widgety
ChatTabContent. Konštanta imageContainer sa vloží
ako grafický prvok do tabu a bude obsahovať buď imgTyping, ak
klient píše, inak circle. Konštantu chatContact
iniciaizujeme v konstruktoru z parametra:
ChatTab(ChatContact chatContact) {
super();
this.chatContact = chatContact;
this.chatContact.getMessages().addListener(this.messagesListener);
loadMessagesAsync();
final ImageView loadingImage = new ImageView();
loadingImage.setImage(new Image(PATH_IMG_LOADING));
container.setContent(loadingImage);
container.setHbarPolicy(ScrollBarPolicy.NEVER);
container.setFitToWidth(true);
setContent(container);
messagesContiainer.heightProperty().addListener((observable, oldValue, newValue) -> {
container.setVvalue(newValue.doubleValue());
});
this.container.focusedProperty().addListener((observable, oldValue, newValue) -> {
chatContact.resetUnreadedMessages();
});
chatContact.resetUnreadedMessages();
circle.setFill(chatContact.getColor());
setGraphic(buildTabGraphic(chatContact.getName()));
chatContact.typingProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
imageContainer.getChildren().setAll(imgTyping);
} else {
imageContainer.getChildren().setAll(circle);
}
});
}
V konstruktoru sa toho deje oveľa viac. Najskôr sa nastaví listener na
prijaté správy od klienta. V tomto Listener budeme transformovať jednotlivé
správy na widgety typu ChatTabContent. Metódou
loadMessagesAsync() načítame asynchrónne všetky doteraz
prijaté a odoslané správy. Než sa načítajú všetky správy, tak by bolo
dobré, aby sme informovali používateľa, že sa niečo deje. K domu slúži
ďalšie riadky kódu, kde vytvoríme obrázok s animáciou načítanie a
vložíme ho ako jediný obsah do ScrollPane.
Ďalej nastavíme listener na výšku kontajnera správ:
messagesContiainer.heightProperty().addListener((observable, oldValue, newValue) -> {
container.setVvalue(newValue.doubleValue());
});
Vždy, keď sa do VBox u vloží nová správa, zavolá sa tento
listener a upraví výšku i ScrollPane.
Druhý listener:
this.container.focusedProperty().addListener((observable, oldValue, newValue) -> {
chatContact.resetUnreadedMessages();
});
Zakaždým, keď klikneme do tabu, tak vyresetuje indikátor neprečítaných správ.
Ďalej necháme vyresetovať všetky neprečítané správy a koliesku
circle nastavíme farbu podľa príslušného kontaktu.
Volaním metódy setGraphic() nastavíme tabu vlastnú grafickú
reprezentáciu. Nakoniec pridáme listener na vlastnosť
typingProperty. Podľa stavu buď budeme zobrazovať animáciu
imgTyping, alebo koliesko circle.
V metóde setGraphic() voláme pomocnú metódu
buildTabGraphic() na zostavenie vlastnej grafickej
reprezentácie:
private HBox buildTabGraphic(String contactName) { final Label lblName = new Label(contactName); imageContainer.getChildren().setAll(circle); imageContainer.setPrefWidth(16); imageContainer.setPrefHeight(16); final HBox graphicContainer = new HBox(imageContainer, lblName); graphicContainer.setAlignment(Pos.CENTER_LEFT); graphicContainer.setSpacing(8); graphicContainer.setPrefHeight(32); HBox.setHgrow(lblName, Priority.ALWAYS); circle.setRadius(8); return graphicContainer; }
Ďalej si vytvoríme súkromnú metódu getPath(), ktorá nám
vráti cestu k správnemu view podľa kontaktu:
private URL getPath(ChatContact from) { return from == this.chatContact ? PATH_CONTENT_INCOMING : PATH_CONTENT_OUTCOMING; }
Metódou addMessage() budeme tvoriť nové widgety
ChatTabContent:
private ChatTabContent addMessage(ChatMessageEntry chatMessage) { final ChatContact contact = chatMessage.getChatContact(); final String message = chatMessage.getMessage(); final FXMLLoader loader = new FXMLLoader(getPath(contact)); ChatTabContent controller = null; try { final Parent parent = loader.load(); controller = loader.getController(); controller.setColor(contact.getColor()); controller.setContactName(contact.getName()); controller.setMessage(message); parent.setUserData(controller); mess agesContiainer.getChildren().add(parent); } catch (IOException e) { e.printStackTrace(); } return controller; }
V konstruktoru sme volali metódu loadMessageAsync(). Teraz ju
implementujeme:
private void loadMessagesAsync() { CompletableFuture.runAsync(() -> { try { Thread.sleep(1000); } catch (InterruptedException ignored) {} this.chatContact.getMessages().forEach(this::addMessage); }, ThreadPool.COMMON_EXECUTOR) .thenAcceptAsync(ignored -> { container.setContent(messagesContiainer); messagesContiainer.getChildren() .stream() .map(node -> (ChatTabContent) node.getUserData()) .filter(Objects::nonNull) .forEach(ChatTabContent::askForResizeTextArea); }, ThreadPool.JAVAFX_EXECUTOR); }
Na začiatku opäť chvíľu počkáme, potom prejdeme všetky správy a
vizualizuje ich. To všetko v "pracovnom" vlákne. V hlavnom vlákne potom
nastavíme do ScrollPane kontajner so správami, teda
VBox. Nakoniec prejdeme všetky správy a požiadame ich o
automatické nastavenie veľkosti.
Nakoniec pridáme triedny konštantu, ktorá bude obsahovať anonymné funkciu, ktorá sa bude starať o pridávaní nových správ:
private final ListChangeListener <? super ChatMessageEntry> messagesListener = c -> { while (c.next()) { if (c.wasAdded()) { for (ChatMessageEntry chatMessageEntry: c.getAddedSubList()) { final ChatTabContent chatTabContent = addMessage(chatMessageEntry); if (chatTabContent != null) { chatTabContent.askForResizeTextArea(); } } } } };
Úprava view pre správy
Je potrebné upraviť súbory /fxml/chat/message_incomming.fxml
a /fxml/chat/message_incomming.fxml. Koreňovému prvku
AnchorPane priradíme kontrolér. Ďalej pridáme novú kontrolku
ImageView, v ktorej budeme zobrazovať načítací animáciu.
Súbor message_incomming.fxml bude po úpravách vyzerať
takto:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<AnchorPane VBox.vgrow="NEVER" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="cz.stechy.chat.widget.ChatTabContent">
<Circle fx:id="circle" fill="DODGERBLUE" layoutX="43.0" layoutY="38.0" radius="29.0" stroke="BLACK" strokeType="INSIDE" AnchorPane.bottomAnchor="8.0" AnchorPane.leftAnchor="8.0" AnchorPane.topAnchor="8.0" />
<Label fx:id="lblFrom" layoutX="79.0" layoutY="5.0" AnchorPane.leftAnchor="80.0" />
<TextArea fx:id="areaMessage" disable="true" editable="false" layoutX="69.0" layoutY="25.0" maxWidth="300.0" prefColumnCount="15" prefRowCount="1" wrapText="true" AnchorPane.bottomAnchor="8.0" AnchorPane.leftAnchor="80.0" AnchorPane.topAnchor="22.0" />
<ImageView fx:id="imgLoading" fitHeight="32.0" fitWidth="32.0" layoutX="137.0" layoutY="21.0" pickOnBounds="true" preserveRatio="true">
<Image url="@../../img/loading.gif" />
</ImageView>
<padding>
<Insets right="8.0" />
</padding>
</AnchorPane>
A jeho grafická reprezentácia ...

Súbor message_outcomming.fxml bude po úpravách vyzerať
takto:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<AnchorPane VBox.vgrow="NEVER" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="cz.stechy.chat.widget.ChatTabContent">
<Circle fx:id="circle" fill="DODGERBLUE" layoutX="557.0" layoutY="38.0" radius="29.0" stroke="BLACK" strokeType="INSIDE" AnchorPane.bottomAnchor="8.0" AnchorPane.rightAnchor="8.0" AnchorPane.topAnchor="8.0" />
<Label fx:id="lblFrom" layoutX="483.0" layoutY="5.0" AnchorPane.rightAnchor="80.0" />
<TextArea fx:id="areaMessage" disable="true" editable="false" layoutX="69.0" layoutY="25.0" maxWidth="300.0" prefColumnCount="15" prefRowCount="1" wrapText="true" AnchorPane.bottomAnchor="8.0" AnchorPane.rightAnchor="80.0" AnchorPane.topAnchor="22.0" />
<ImageView fx:id="imgLoading" fitHeight="32.0" fitWidth="32.0" layoutX="137.0" layoutY="21.0" pickOnBounds="true" preserveRatio="true">
<Image url="@../../img/loading.gif" />
</ImageView>
<padding>
<Insets left="8.0" />
</padding>
</AnchorPane>
A jeho grafická reprezentácia ...

To by bolo pre dnešné lekciu všetko. Nabudúce, v lekcii Java chat - Klient - Dokončenie 2. časť ,
prepojíme ChatTab s hlavným kontrolórom.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 16x (141.02 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java
