在JavaFX中,我该如何编程将ScrollPane自动滚动到底部?

3

我一直在为自己和朋友开发一个小型聊天客户端,在学习Java和JFX 8的同时。不幸的是,由于某些原因,我无法以编程方式滚动到包含聊天消息的ScrollPane的底部。

我有一个我简单称之为scrollToBottom()的方法,它执行以下代码:

public void scrollToBottom() {
    Platform.runLater(() -> this.getChatView().vvalueProperty().setValue(1.0));
}

我有一个按钮,使用这种方法滚动到底部是有效的,但是任何其他编程方法(甚至从其他方法触发按钮)都不能正确更新滚动窗格的滚动条。然而,当我在所谓的滚动后调用scrollpane上的getVvalue()时,它返回我试图通过滚动来获取的正确值。滚动窗格只是没有滚动到应该的值。

下面是应用程序中唯一真正相关的类 - ChatBox类型只是VBox的扩展,仅使用了扩展类型的Text。

public class MainScreenController {

    //Lists

    //Buttons
    private Button logoutButton;
    private Button btn1 = new Button("Test Button");
    private Button scrollButton = new Button("Scroll Button");

    //Numbers

    //Booleans
    private boolean isHosting;

    //Strings
    private String username = "";

    //Scene
    private Stage window;
    private GridPane layout = new GridPane();

    //Other Objects
    private TextArea chatField = new TextArea();
    private Label usernameLabel;
    private TextArea usersArea = new TextArea("Connected users: ");
    private VBox firstColumn = new VBox(10);
    private VBox secondColumn = new VBox(10);
    private ImageView mediaColumn = new ImageView();
    private Server server = new Server();
    private Client client = new Client();
    private TextField dlField = new TextField();
    private AudioHandler audioHandler = new AudioHandler();
    private ChatBox chatBox = new ChatBox();
    private ScrollPane chatView = new ScrollPane(this.chatBox);
    private HBox buttonBox = new HBox(10);

    private void initChatView() {

        this.getChatView().setMinSize(500, 500);
        this.getChatView().setPrefSize(500, 500);
        this.getChatView().setMaxSize(500, 500);
        this.getChatView().setStyle("-fx-focus-color: transparent; -fx-background-color: gainsboro");
        this.chatView.hbarPolicyProperty().set(ScrollBarPolicy.NEVER);
        this.chatView.vbarPolicyProperty().set(ScrollBarPolicy.AS_NEEDED);

        /*this.chatView.vvalueProperty().addListener(e -> {
              if(this.chatView.getVvalue() != 1) {
                  System.out.println("Pre: " + this.chatView.getVvalue());
                  this.chatView.vvalueProperty().set(1);
                  System.out.println("Post: " + this.chatView.getVvalue());
                }
        });*/

        this.scrollButton.setOnAction(e -> {
            scrollToBottom();
        });

    }

    private void initSecondColumn() {

        this.secondColumn.setStyle("-fx-background-color: gainsboro");
        this.secondColumn.setPrefSize(525, 550);
        this.secondColumn.setMinSize(450, 550);
        this.secondColumn.setMaxSize(525, 550);

        Button dlButton = new Button("Download link:");
        dlButton.setStyle("-fx-focus-color: transparent");
        dlButton.setOnAction(e -> {
            if (!this.dlField.getText().equals(null) || !this.dlField.getText().equals(null)) {
                try {
                    FileHandler.downloadFile(this.window, this.dlField.getText());
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                this.dlField.clear();
            }
        });
        this.dlField.setPrefSize(450, 20);
        this.dlField.setMinSize(450, 20);
        this.dlField.setMaxSize(450, 20);

        this.mediaColumn.setFitHeight(475);
        this.mediaColumn.setPreserveRatio(true);
        this.mediaColumn.setStyle("-fx-border-color: red");
        this.mediaColumn.prefWidth(450);
        this.mediaColumn.prefHeight(475);
        this.mediaColumn.minWidth(450);
        this.mediaColumn.minHeight(475);
        this.mediaColumn.maxWidth(450);
        this.mediaColumn.maxHeight(475);
        this.secondColumn.getChildren().addAll(dlButton, this.dlField, mediaColumn);

    }

    private void initUsersArea() {

        this.usersArea.setPrefSize(500, 75);
        this.usersArea.setMinSize(500, 75);
        this.usersArea.setMaxSize(500, 75);
        this.usersArea.setEditable(false);
        this.usersArea.setStyle("-fx-focus-color: transparent; -fx-background-color: gainsboro");

    }

    public void scrollToBottom() {
        Platform.runLater(() -> this.getChatView().vvalueProperty().setValue(1.0));
        System.out.println(this.chatView.getVvalue());
    }

    public MainScreenController(GridPane layout, Stage window, Scene currentScene, Scene nextScene, WindowController windowController) throws URISyntaxException, IOException {

        new File(FileHandler.downloadsPath).mkdirs();
        this.layout = layout;
        this.window = window;
        this.initChatView();
        FileHandler.readLog(this.getChatBox());

    }

    private void initUsernameLabel() {

        this.usernameLabel = new Label();
        this.usernameLabel.setStyle("-fx-border-color: black; -fx-background-color: silver; -fx-focus-color: transparent");
        this.usernameLabel.setText(" Logged in as ");
        this.usernameLabel.setTextFill(Color.BLUE);

    }

    private void initLayout() {

        this.layout.setPadding(new Insets(10, 10, 10, 10));
        this.layout.setVgap(10);
        this.layout.setHgap(10);
        this.layout.getChildren().addAll(this.firstColumn, this.secondColumn);
        GridPane.setColumnIndex(this.firstColumn, 0);
        GridPane.setColumnIndex(this.secondColumn, 1);
        GridPane.setValignment(secondColumn, VPos.BOTTOM);
        this.buttonBox.getChildren().addAll(this.logoutButton, this.scrollButton);
        this.firstColumn.getChildren().addAll(this.buttonBox, this.usernameLabel, this.usersArea, this.chatView, this.getChatField());

    }

    public void initBtn1() {
        btn1.setStyle("-fx-focus-color: transparent");
        GridPane.setConstraints(btn1, 1, 0);
        GridPane.setValignment(btn1, VPos.BASELINE);
        this.layout.getChildren().add(btn1);
        btn1.addEventHandler(ActionEvent.ACTION, e -> {
            boolean b = false;
            if (!b) {
                audioHandler.startRecording();
                b = !b;
            } else if (b) {
                audioHandler.stopRecording();
                b = !b;
            }
        });
    }

    public void addMessage(String msg, String color) {

        FileHandler.writeToChatLog(msg);

        if (!msg.startsWith("*!") && !msg.startsWith("/")) {
            Platform.runLater(() -> {
                this.getChatBox().addText(new ChatText(msg, color));
                this.scrollButton.arm();
                this.scrollButton.fire();
            });
        }

    }

    private void initChatField() {

        this.getChatField().setPrefSize(500, 10);
        this.getChatField().setMaxHeight(10);
        this.getChatField().autosize();
        this.getChatField().setWrapText(true);
        this.getChatField().addEventHandler(KeyEvent.KEY_PRESSED, key -> {
            if (key.getCode() == KeyCode.ENTER) {
                key.consume();
                if (this.getChatField().getText().startsWith("/")) {
                    CommandParser.parse(this.getChatField().getText(), this);
                } else {
                    try {
                        this.getClient().getClientSendingData().writeUTF(this.getChatField().getText().trim());
                        this.getChatField().clear();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        this.getChatField().setStyle("-fx-background-color: gainsboro");

    }

    private void initLogoutButton() {

        this.logoutButton = new Button();
        this.logoutButton.setText("Log out");
        this.logoutButton.setOnAction(e -> {
            try {
                this.client.getClientSendingData().writeUTF("*![System] " + SystemInfo.getDate() + ": " + this.client.getClientName() + " has disconnected.");
                System.out.println("Logging out.");
                window.close();
                new ChatClient().start(new Stage());
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });
        this.logoutButton.setStyle("-fx-focus-color: transparent");

    }

    public void initMainScreen() {

        initUsernameLabel();
        initLogoutButton();
        initChatField();
        initLayout();
        initUsersArea();
        initBtn1();
        initSecondColumn();

    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getUsername() {
        return this.username;
    }

    public Label getUsernameLabel() {
        return this.usernameLabel;
    }

    public GridPane getLayout() {
        return this.layout;
    }

    public void setIsHosting(boolean b) {
        this.isHosting = b;
    }

    public boolean getIsHosting() {
        return this.isHosting;
    }

    public Server getServer() {
        return this.server;
    }

    public void setServer(Server hostServer) {
        this.server = hostServer;
    }

    public Client getClient() {
        return this.client;
    }

    public void setClient(Client client) {
        this.client = client;
    }

    public ScrollPane getChatView() {
        return chatView;
    }

    public void setChatView(ScrollPane chatView) {
        this.chatView = chatView;
    }

    public TextArea getChatField() {
        return chatField;
    }

    public void setChatField(TextArea chatField) {
        this.chatField = chatField;
    }

    public TextArea getUsersArea() {
        return this.usersArea;
    }

    public ChatBox getChatBox() {
        return chatBox;
    }

    public void setChatBox(ChatBox chatBox) {
        this.chatBox = chatBox;
    }

}

如果你有任何其他一般性建议(我刚开始学习Java),欢迎提供所有技巧。 :) 谢谢!


1
也许是这个链接(https://dev59.com/wGIj5IYBdhLWcg3wYUGQ),或者是这个链接(https://dev59.com/83PYa4cB1Zd3GeqPehKz),又或者是这个链接(https://dev59.com/LGrWa4cB1Zd3GeqP9Edj),甚至可能是这个链接(http://javafx-demos.googlecode.com/svn-history/r77/trunk/javafx-demos/src/main/java/com/ezest/javafx/demogallery/internet/ScrollPaneScrollToEnd.java)。 - MadProgrammer
@MadProgrammer 以上的方法似乎都不起作用 - 我确实尝试过。不幸的是,问题似乎不在于我完全无法滚动,而是由于某种我不理解的原因,滚动仅在某些情况下起作用。 - Ben Sixel
1
如果您展示代码,请确保它是一个SSCCE - 在您描述与普通fx的差异时,所有的“just”可能会使一切变得有所不同,这可能是一个简短的例子,_只包含一个纯文本区域(可能在vbox中)和所需的初始内容以及触发滚动的按钮。 - kleopatra
3个回答

2

我也遇到了使用ScrollPane和MasonryPane(来自JFoenix)时出现的那个bug。我找到的解决办法是调用ScrollPane及其内容的layout()方法。层次结构:ScrollPane -> MasonryPane。

masonryPane.getChildren().addListener((ListChangeListener<Node>) c -> {
            masonryPane.layout();
            scrollPane.layout();
            scrollPane.setVvalue(1.0f);
        });

1
我所做的将TextArea中的文本滚动到底部的方法是将光标设置在文本的末尾,例如:
textArea.positionCaret(textArea.getText().length());

事实上,为了确保TextArea中的文本不会水平向左滚动(例如,当TextArea中的最后一行非常长时),我将插入符号设置在文本中最后一个"\n"之后,就像这样:
String content = textArea.getText();
int indexOfLastLineFeed = content.lastIndexOf("\n");
if (indexOfLastLineFeed == -1)
    textArea.positionCaret(content.length());
else
    textArea.positionCaret(Math.min(indexOfLastLineFeed + 1, content.length()));

-2
我发现问题是由于多线程引起的。对于任何感兴趣的人,请尝试在执行scrollToBottom()调用的线程上添加一个非常小的Thread.sleep();我选择了10毫秒作为最低值,似乎可以工作。感谢任何试图帮助的人。

2
Thread.sleep 这样的方法来解决线程相关问题几乎总是错误的解决方案,即使它似乎可行。 - jewelsea
@jewelsea,你有什么替代建议吗? - Ben Sixel
2
我建议你按照kleopatra的建议,在你的问题中放置一个SSCCE(在stackoverflow术语中为mcve);即最小化,可编译,可执行的代码,仅复制您的问题而不复制其他内容。然后,对于某人来说,理解您正在尝试做什么,您的问题是什么以及创建一个可以建议合理替代方案的答案将更简单。 - jewelsea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接