IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

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.

Zoznam prihlásených klientov - Server pre klientskej aplikácie v Jave

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 ...

Grafická reprezentácia prichádzajúce správy - Server pre klientskej aplikácie v Jave

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 ...

Grafická reprezentácia odoslané správy - Server pre klientskej aplikácie v Jave

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

 

Predchádzajúci článok
Java chat - Server - Chat plugin
Všetky články v sekcii
Server pre klientskej aplikácie v Jave
Preskočiť článok
(neodporúčame)
Java chat - Klient - Dokončenie 2. časť
Článok pre vás napísal Petr Štechmüller
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity