Vánoční nadílka Vánoční nadílka
Vánoční akce! Daruj lepší budoucnost blízkým nebo sobě. Až +50 % zdarma na dárkové poukazy. Více informací

Lekce 23 - Java chat - Klient - Dokončení 1. část

Java Server pro klientské aplikace Java chat - Klient - Dokončení 1. část

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, Java chat - Server - Chat plugin, jsme vytvořili chat plugin pro server. V první části dnešního Java tutoriálu zobrazíme přihlášené uživatele v GUI. V druhé části vytvoříme widgety reprezentující konverzaci a její obsah.

Zobrazení přihlášených uživatelů

Uživatele budeme zobrazovat v ListView, které se nachází v souboru main.fxml a jeho reference je uložena v kontroleru MainController. Nejdříve toto ListView natypujeme na třídu ChatContact:

@FXML
private ListView<ChatContact> lvContactList;

Dále vytvoříme v MainControlleru novou instanční konstantu typu IChatService a hned ji vytvoříme instanci:

private final IChatService chatService = new ChatService(communicator);

V metodě handleConnect() nastavíme ConnectControlleru chatService.

controller.setChatService(chatService);

Dále vyplníme tělo metody initialize().

@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
    lvContactList.setCellFactory(param -> new ChatEntryCell());
    chatService.getClients().addListener(this.chatClientListener);
}

V metodě zatím děláme dvě věci:

  • nastavíme továrnu záznamů pro ListView
  • přidáme posluchače klientů v chatService

Nyní vytvoříme konstantu, která bude obsahovat anonymní funkci, která se bude spouštět při změně klientů 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 novou třídu ChatEntryCell, která bude reprezentovat 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 obsahovat kolečko s náhodně vygenerovanou barvou, dále jméno uživatele a vpravo se bude zobrazovat počet nepřečtených zpráv od daného uživatele.

Seznam přihlášených klientů

ChatTabContent

Jednotlivé zprávy budou reprezentovány třídou ChatTabContent. Tyto zprávy mají definované vlastní view v souborech:

  • /fxml/chat/message_incomming.fxml
  • /fxml/chat/message_outcomming.fxml

Jediný rozdíl v souborech je rozmístění prvků, jinak jsou totožné. Každá zpráva bude obsahovat kolečko reprezentující uživatele, jméno uživatele a samotný obsah zprávy.

V balíčku widget založíme novou třídu 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 zprávy budeme zobrazovat se zpožděním, aby se velikost TextArea nastavila správně a my nemuseli používat ScrollBar pro přečtení jednotlivé zprávy. Zpoždění je opět vyřešeno za pomocí CompletableFuture, kde na začátku jednoduše počkáme jednu vteřinu (v pracovním vlákně). Po uplynutí této doby vyhledáme v TextArea metodou lookup() text a podle délky textu nastavíme výšku. Nakonec schováme obrázek s načítací animací.

ChatTab

Nyní se dostáváme k samotnému chatovacímu oknu. V MainControlleru máme TabPane, ve kterém budeme zobrazovat jednotlivé konverzace. Každá konverzace bude v jednom tabu. V balíčku widget založíme novou třídu ChatTab:

public class ChatTab extends Tab{}

Nejdříve vytvoříme konstanty, do kterých uložíme cesty k důležitým souborům.

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 pro načítání a indikátor psaní jsou přiloženy ve zdrojových na konci článku.

Dále vytvoříme instanční konstanty:

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 obsahovat ScrollPane s VBoxem. Do VBoxu se budou vkládat jednotlivé zprávy, tedy widgety ChatTabContent. Konstanta imageContainer se vloží jako grafický prvek do tabu a bude obsahovat buď imgTyping, pokud klient píše, jinak circle. Konstantu chatContact iniciaizujeme v konstruktoru z parametru:

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 se toho děje mnohem více. Nejdříve se nastaví listener na přijaté zprávy od klienta. V tomto listeneru budeme transformovat jednotlivé zprávy na widgety typu ChatTabContent. Metodou loadMessagesAsync() načteme asynchronně všechny dosud přijaté a odeslané zprávy. Než se načtou všechny zprávy, tak by bylo dobré, abychom informovali uživatele, že se něco děje. K domu slouží další řádky kódu, kde vytvoříme obrázek s animací načítání a vložíme ho jako jediný obsah do ScrollPane.

Dále nastavíme listener na výšku kontejneru zpráv:

messagesContiainer.heightProperty().addListener((observable, oldValue, newValue) -> {
    container.setVvalue(newValue.doubleValue());
});

Vždy, když se do VBoxu vloží nová zpráva, zavolá se tento listener a upraví výšku i ScrollPane.

Druhý listener:

this.container.focusedProperty().addListener((observable, oldValue, newValue) -> {
    chatContact.resetUnreadedMessages();
});

Pokaždé, když klikneme do tabu, tak vyresetujeme indikátor nepřečtených zpráv.

Dále necháme vyresetovat všechny nepřečtené zprávy a kolečku circle nastavíme barvu dle příslušného kontaktu.

Voláním metody setGraphic() nastavíme tabu vlastní grafickou reprezentaci. Nakonec přidáme listener na vlastnost typingProperty. Podle stavu buď budeme zobrazovat animaci imgTyping, nebo kolečko circle.

V metodě setGraphic() voláme pomocnou metodu buildTabGraphic() k sestavení vlastní grafické reprezentace:

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;
}

Dále si vytvoříme soukromou metodu getPath(), která nám vrátí cestu ke správnému view podle kontaktu:

private URL getPath(ChatContact from) {
    return from == this.chatContact ? PATH_CONTENT_INCOMING : PATH_CONTENT_OUTCOMING;
}

Metodou addMessage() budeme tvořit 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 jsme volali metodu loadMessageAsync(). Nyní ji 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čátku opět chvíli počkáme, poté projdeme všechny zprávy a vizualizujeme je. To vše v "pracovním" vlákně. V hlavním vlákně pak nastavíme do ScrollPane kontejner se zprávami, tedy VBox. Nakonec projdeme všechny zprávy a požádáme je o automatické nastavení velikosti.

Nakonec přidáme třídní konstantu, která bude obsahovat anonymní funkci, která se bude starat o přidávání nových zprá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 pro zprávy

Je potřeba upravit soubory /fxml/chat/message_incomming.fxml a /fxml/chat/message_incomming.fxml. Kořenovému prvku AnchorPane přiřadíme kontroler. Dále přidáme novou kontrolku ImageView, ve které budeme zobrazovat načítací animaci.

Soubor message_incomming.fxml bude po úpravách vypadat 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á reprezentace...

Grafická reprezentace příchozí zprávy

Soubor message_outcomming.fxml bude po úpravách vypadat 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á reprezentace...

Grafická reprezentace odeslané zprávy

To by bylo pro dnešní lekci vše. Příště, v lekci Java chat - Klient - Dokončení 2. část, propojíme ChatTab s hlavním kontrolerem.


 

Stáhnout

Staženo 2x (143.8 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

 

Článek pro vás napsal Petr Štechmüller
Avatar
Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!
Autor se věnuje primárně programování v Jave, ale nebojí se ani webových technologií.
Miniatura
Předchozí článek
Java chat - Server - Chat plugin
Miniatura
Všechny články v sekci
Server pro klientské aplikace v Javě
Aktivity (4)

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!