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é 15x (141.02 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java