为什么 JavaFX TextArea 中的 selectRange() 有时不突出显示所选内容?

Why does selectRange() in JavaFX TextArea sometimes not highlight the selection?

提问人:nick16384 提问时间:9/11/2023 更新时间:9/11/2023 访问量:50

问:

我在 JavaFX 主类中编写了以下代码:

package jfxTest;

import java.util.Random;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    private static int selectionIndex = 0;
    private static TextArea textArea;

    @Override
    public void start(Stage primaryStage) {
        System.out.println("Starting JavaFX Window...");
        StackPane rootPane = new StackPane();
        textArea = new TextArea();

        textArea.setText("TEST");
        textArea.setEditable(false);
        createRandomOptions(8);

        textArea.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
            if (event.getCode().equals(KeyCode.UP)) {
                select(selectionIndex <= 0 ? 0 : selectionIndex - 1);
            }
            if (event.getCode().equals(KeyCode.DOWN)) {
                String[] lines = textArea.getText().split("\n");
                select(selectionIndex >= lines.length - 1 ? lines.length - 1 : selectionIndex + 1);
            }
        });

        rootPane.getChildren().add(textArea);
        Scene scene = new Scene(rootPane, 900, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
        System.out.println("Created window.");
        System.out.println("\nNow press up and down keys to navigate:\n"
                + "Notice, that although a new selection is displayed in the console, it is not\n"
                + "highlighted in the window itself. Even a call to getSelectedText() works fine.\n");
    }
    
    /**
     * Creates a bunch of random options in textArea.
     * @param newOptionCount
     */
    private static void createRandomOptions(int newOptionCount) {
        for (int i = 0; i < newOptionCount; i++) {
            textArea.appendText("\nSEL" + (new Random().nextInt(10000)));
        }
    }
    
    private static void select(int newSelectionIndex) {
        String[] lines = textArea.getText().split("\n");
        
        System.out.println("New selection index: " + newSelectionIndex);
        selectionIndex = newSelectionIndex;
        
        // Determine selection indexes
        int selectionStart = 0;
        int selectionEnd = 0;
        for (int i = 0; i < newSelectionIndex; i++) {
            selectionStart += lines[i].length() + 1;
        }
        selectionEnd = selectionStart + lines[newSelectionIndex].length();
        
        // Does not work. Selection does need to be applied twice by the user to be actually highlighted.
        textArea.selectRange(selectionStart, selectionEnd);
        
        System.out.println("Selected text: " + textArea.getSelectedText() + "\n");
    }
}

我知道它看起来一团糟,并不完全干净,但你应该明白这一点。 现在问题如下: 当我使用箭头键(特别是 UP 和 DOWN)导航时,只有当用户应用两次(仅在最顶部和底部)时,选择才会变得可见,尽管每次都会调用 to。TextAreaselectRange()

为什么会这样?是 JavaFX 库中的错误,还是我遗漏了什么?

我已经搜索了几次这个问题,但还没有找到任何结果。 我还没有使用 AWT Swing 对此进行测试,但我非常有信心,使用它可以正常工作。

我的 Java 是带有 JavaFX 版本 19 的 OpenJDK 19。

Java JavaFX TextArea 选择

评论

0赞 nick16384 9/11/2023
@James_D可能是我没有考虑过的好选择。您能否给我一个简单的实现,其中包含一些选项可供选择,以便我了解它是如何工作的?ListView
1赞 James_D 9/11/2023
添加了示例(使用 a 和使用 a 作为答案来执行此操作。ListViewTextArea

答:

4赞 James_D 9/11/2023 #1

将低级事件处理(如鼠标和键盘事件处理程序)添加到高级 UI 控件从来都不是一个特别好的主意。您始终面临干扰控件的内置功能的风险,或者内置功能干扰事件处理程序的风险,从而导致无法提供所需的功能。

在您的例子中,后者正在发生:已经实现了在按下键时更改选择。如果在没有修饰键的情况下按下箭头键,则选择将被清除。此默认行为发生在调用按键之后,因此默认行为是您看到的行为。TextArea

您可以通过添加

textArea.selectionProperty().addListener((obs, oldSel, newSel) -> 
    System.out.printf("Selection change: %s -> %s%n", oldSel, newSel)
);

请注意,您可以通过添加键事件处理程序中的两个块来解决此问题,但我认为这不是一个特别好的解决方案:恕我直言,下面的解决方案更好。event.consume()if() { ... }

看起来您只是在这里使用了错误的控件。如果你有一个不可编辑的,你把每一行都看作是一个不同的项目,那么听起来你正在尝试重新实现一个,它已经实现了诸如选择单个项目之类的事情。您可以使用以下方法重新实现您的示例:TextAreaListView

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import java.util.Random;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    private ListView<String> listView;

    @Override
    public void start(Stage primaryStage) {
        System.out.println("Starting JavaFX Window...");
        StackPane rootPane = new StackPane();
        listView = new ListView<>();


        listView.getItems().add("TEST");
        createRandomOptions(8);


        rootPane.getChildren().add(listView);
        Scene scene = new Scene(rootPane, 900, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
        System.out.println("Created window.");
        System.out.println("\nNow press up and down keys to navigate:\n"
                + "Notice, that although a new selection is displayed in the console, it is not\n"
                + "highlighted in the window itself. Even a call to getSelectedText() works fine.\n");
    }


    /**
     * Creates a bunch of random options in textArea.
     * @param newOptionCount
     */
    private void createRandomOptions(int newOptionCount) {
        for (int i = 0; i < newOptionCount; i++) {
            listView.getItems().add("SEL" + (new Random().nextInt(10000)));
        }
    }

}

如果确实要修改在文本区域中选择的内容,请使用带有修改选择的过滤器。这看起来像这样(不过,我再次认为这只是重新发明轮子)。请注意,这也符合您(大概)在鼠标单击时想要的方式:TextFormatter

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.IndexRange;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import java.util.Random;

public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }

    private TextArea textArea;

    @Override
    public void start(Stage primaryStage) {
        System.out.println("Starting JavaFX Window...");
        StackPane rootPane = new StackPane();
        textArea = new TextArea();

        textArea.selectionProperty().addListener((obs, oldSel, newSel) -> System.out.printf("Selection change: %s -> %s%n", oldSel, newSel));

        textArea.setText("TEST");
        textArea.setEditable(false);
        createRandomOptions(8);

        textArea.setTextFormatter(new TextFormatter<String>(this::modifySelection));

        rootPane.getChildren().add(textArea);
        Scene scene = new Scene(rootPane, 900, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
        System.out.println("Created window.");
        System.out.println("\nNow press up and down keys to navigate:\n"
                + "Notice, that although a new selection is displayed in the console, it is not\n"
                + "highlighted in the window itself. Even a call to getSelectedText() works fine.\n");
    }
    
    private TextFormatter.Change modifySelection(TextFormatter.Change change) {
        IndexRange selection = change.getSelection();
        String[] lines = change.getControlNewText().split("\n");
        int lineStart = 0 ;
        for (String line : lines) {
            int lineEnd = lineStart + line.length();
            if (lineStart <= selection.getStart() && lineEnd >= selection.getStart()) {
                change.setAnchor(lineStart);
            }
            if (lineStart <= selection.getEnd() && lineEnd >= selection.getEnd()) {
                change.setCaretPosition(lineEnd);
            }
            lineStart += line.length() + 1; // +1 to account for line terminator itself
        }
        return change;
    }

    /**
     * Creates a bunch of random options in textArea.
     * @param newOptionCount
     */
    private void createRandomOptions(int newOptionCount) {
        for (int i = 0; i < newOptionCount; i++) {
            textArea.appendText("\nSEL" + (new Random().nextInt(10000)));
        }
    }

}

评论

1赞 nick16384 9/11/2023
谢谢!两个版本都可以完美运行,没有任何问题。我将使用 ListView,因为这是我想要的直接实现。解释也非常详细。