JavaFX - TextArea 的掩码文本

Posted

技术标签:

【中文标题】JavaFX - TextArea 的掩码文本【英文标题】:JavaFX - Mask text of a TextArea 【发布时间】:2018-03-19 04:13:18 【问题描述】:

我想知道是否有办法在 JavaFX 中屏蔽 TextArea 的文本。 例如,使用像PasswordField 这样的“子弹”密码字符来屏蔽文本。对于TextField,有一个很好用的maskText() 方法。此方法对TextArea 没有用处。 我能做些什么?

注意:我希望 getText()setText() 方法必须适用于明文,而不是屏蔽文本。就像PasswordField 一样。

编辑 这是我用来达到结果的方法,但不幸的是没有成功。

我的自定义TextArea 类:

public class PasswordArea extends TextArea 

    @Override
    protected Skin<?> createDefaultSkin() 
        return new PasswordAreaSkin(this); //my custom skin
    

用于自定义TextArea的自定义皮肤:

public class PasswordAreaSkin extends TextAreaSkin 
    public PasswordAreaSkin(PasswordArea control) 
        super(control);
    

    //here I override the maskText method to mask the text
    @Override
    protected String maskText(String text) 
        int n = text.length();
        StringBuilder passwordBuilder=new StringBuilder(n);
        for(int i = 0; i < n; i++) 
            passwordBuilder.append('\u2022'); //append 'bullet' char
        

        return passwordBuilder.toString();
    

【问题讨论】:

由于没有内置方法来屏蔽文本,您必须自己实现一个。为什么以及如何屏蔽TextArea 中的文本?整个文本还是部分文本(例如单个 Strings)? 我希望TextArea 中的所有文本都被PasswordField 等“子弹”字符所掩盖。我试图创建一个自定义的TextArea,但未成功覆盖maskText() 方法。 你不能重写超类中不存在的方法,可以吗?请在一个最小的工作(或失败)示例中提供您迄今为止编写的代码。我的第一种方法是扩展TextArea,它有一个成员(可能只是一个String),它包含真实的文本,并编写一个方法maskText(),用一个项目符号替换每个字符。 maskText() 方法是 TextInputControlSkin 类的方法我创建了一个自定义 TextArea,其自定义皮肤扩展了 TextAreaSkin(这是 TextInputControlSkin 的子类)跨度> 这里提供代码的另一个原因 ;-) 展示你的方法,如果它只是包含一个小错误,你很有可能与这个社区一起解决这个问题。 【参考方案1】:

您想要的问题是 TextArea 不是为此功能而构建的,至少在 JDK 8 中(JDK 9 添加了公共皮肤 API,例如 TextAreaSkin)。具体来说,它的皮肤TextAreaSkin 不利于掩蔽机制。

TextFieldSkin 通过将可视文本节点的textProperty 绑定到组件的textProperty 来进行屏蔽。因此,对组件“真实”文本的任何更改都会体现在可视组件的文本中以及适当的屏蔽修改(maskText 方法):

textNode.textProperty().bind(new StringBinding() 
     bind(textField.textProperty()); 
    @Override protected String computeValue() 
        return maskText(textField.textProperty().getValueSafe());
    
);

TextAreaSkin 使用一组 Text 节点作为其视觉对象,尽管在 JDK 8 中只使用了 1 个节点。视觉文本的更改是通过侦听组件文本的更改来进行的:

textArea.textProperty().addListener(observable -> 
    invalidateMetrics();
    ((Text)paragraphNodes.getChildren().get(0)).setText(textArea.textProperty().getValueSafe());
    contentView.requestLayout();
);

我们可以使用它来监听视觉文本的变化并自行更新。下面是一个实现的工作示例。 maskText 方法主要是从TextFieldSkin 复制而来。我们使用反射来访问可视文本表示节点,然后使用当前文本(例如,从文本区域构造函数)更新它并注册更新侦听器。

public class Test extends Application 

    @Override
    public void start(Stage stage) throws Exception 
        String s = "some times there are\nmore strings\n\nin here";
        TextArea ta = new TextArea(s);
        ta.setSkin(new TextAreaMaskSkin(ta));

        TextArea view = new TextArea();
        view.textProperty().bind(ta.textProperty());

        Scene scene = new Scene(new HBox(view, ta));
        stage.setScene(scene);
        stage.show();
    

    private static class TextAreaMaskSkin extends TextAreaSkin 

        public TextAreaMaskSkin(TextArea textArea) throws Exception 
            super(textArea);
            Field field = TextAreaSkin.class.getDeclaredField("paragraphNodes");
            field.setAccessible(true);
            Group group = (Group) field.get(this);
            Text text = (Text) group.getChildren().get(0);
            text.setText(maskText(textArea.textProperty().getValueSafe()));
            text.textProperty().addListener(o -> text.setText(maskText(textArea.textProperty().getValueSafe())));
        

        @Override
        protected String maskText(String txt) 
            int n = txt.length();
            StringBuilder passwordBuilder = new StringBuilder(n);
            for (int i = 0; i < n; i++) 
                if (txt.charAt(i) == '\n') 
                    passwordBuilder.append('\n');
                 else 
                    passwordBuilder.append(TextFieldSkin.BULLET);
                
            
            return passwordBuilder.toString();
        
    

    public static void main(String[] args) 
        launch(args);
    

【讨论】:

您使用的是 jdk8 还是 jdk9?因为在 jdk9 中我尝试使用反射,但是在新的模块系统中我得到了一些例外。我还没有完全理解如何纠正反射和模块系统的工作。 @Vin 这是8。你没有提到你使用的是什么版本。 9. 需要打开你要访问的模块。 我发布的解决方案适用于 jdk 8 和 jdk9。唯一的“小故障”是多个明文行转换为单个蒙版行(现在对我来说总比没有好)。 这个也适用于两个版本,你只需要打开模块。我不明白你的“小故障”:你想要发生什么? 我缩进说,如果我写:sometimes\nmore text\nin 字符串,被屏蔽的文本会导致 ********************** *********,人在同一行。我想要的,你解释得很好,因为它不可能发生,是被屏蔽的文本分布在三行上。【参考方案2】:

您可以让每个字符都显示为“子弹”,而不是单独的字符串实际上是文本。

【讨论】:

它无法工作,因为我必须使用 setText() 方法,其中包含作为文本长度的“项目符号”字符的字符串。但是这种方法会破坏将返回掩码文本的getTex() 方法(我希望getText() 改为返回明文)。此外,在 JavaFX 中,setText()getText() 是最终方法,因此它们不能被覆盖。【参考方案3】:

这是一个可以做你想做的事情(至少在我可以遵循你的愿望的范围内......)。它使用ChangeListener 并在存储原始文件的同时操作输入。如需进一步操作或使用,请自行扩展代码。啊,顺便说一句:现在不需要Skin,但可以随意应用它,屏蔽是在PasswordArea 中完成的。这可能不是最有效的解决方案,但它是有效的(当在 Main.java 中使用时,就像在这个答案末尾发布的那样)。

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextArea;

public class PasswordArea extends TextArea 

    private StringBuilder original = new StringBuilder();
    private StringBuilder masked = new StringBuilder();

    public PasswordArea() 
        this.textProperty().addListener(new ChangeListener<String>() 

        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) 
            int oldLength = oldValue.length();
            int newLength = newValue.length();

            if (newLength == oldLength) 
                // obviously an unnecessary case to be checked
             else if (newLength < oldLength) 
                // last character deleted, so delete the last one of each, original and masked text
                original.delete(newLength, oldLength);
                masked.delete(newLength, oldLength);
             else 
                // one character added, so just replace that one
                char c = newValue.toCharArray()[newLength - 1];
                if (Character.isSpaceChar(c)) 
                    original.append(c);
                    masked.append(c);
                 else if (c == '\u2022') 

                 else 
                    masked.append('\u2022');
                    original.append(c);
                
            
            // this output is just for checking the state of the original
            System.out.println(original.toString() + "\t--->\t" + masked.toString());
            textProperty().set(masked.toString());
        
    );

这里是Main.java

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


public class Main extends Application 
    @Override
    public void start(Stage primaryStage) 
        try 
            StackPane root = new StackPane();
            Scene scene = new Scene(root,400,400);
            PasswordArea passwordArea = new PasswordArea();
            root.getChildren().addAll(passwordArea);
            primaryStage.show();
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();
         catch(Exception e) 
            e.printStackTrace();
        
    

    public static void main(String[] args) 
        launch(args);
    

【讨论】:

我可以在此更改侦听器中复制我的maskText() 代码来获得您的代码的结果。问题是 getText() 方法,在这种情况下,返回蒙版文本,而不是我想要的明文。【参考方案4】:

我解决了。 我找到了这个解决方案。它可以工作,但应该在某些条件下进行测试。 然而这是代码,它只涉及皮肤。

public class PasswordAreaSkin extends TextAreaSkin 
    public PasswordAreaSkin(PasswordArea control) 
        Text textNode=getTextNode();
        textNode.textProperty().addListener(obs -> 
            textNode().setText(
                maskText(control.textProperty().getValueSafe()));
        );
    

    @Override
    protected String maskText(String text) 
        int n = txt.length();
        StringBuilder passwordBuilder=new StringBuilder(n);
        for(int i = 0; i < n; i++) 
            passwordBuilder.append('\u2022'); //append 'bullet' char
        

        return passwordBuilder.toString();
    

    private Text getTextNode() 
        //WARNING: call ONLY in the constructor because 
        //children list could change
        Region content=
            ((Region)((ScrollPane)getChildren().get(0)).getContent());
        Group g=(Group)content.getChildrenUnmodifiable().get(1);
        return (Text)g.getChildren().get(0);
    

通过这种方式,文本在控件中被屏蔽,但getText() 返回明文,setText() 使用明文并在 ui 中屏蔽它(这就是我要找的)

唯一的问题是我绑定到一个实现细节,即子列表中 Text 节点的位置。

【讨论】:

【参考方案5】:
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class PasswordArea extends TextArea 

    private final static List<String> text = new ArrayList<>();

    private final static List<KeyCode> allowedKeys = Arrays.asList(KeyCode.ENTER, KeyCode.SPACE, KeyCode.BACK_SPACE, KeyCode.A, KeyCode.B, KeyCode.C, KeyCode.D, KeyCode.E, KeyCode.F,
            KeyCode.G, KeyCode.H, KeyCode.I, KeyCode.J, KeyCode.K, KeyCode.L, KeyCode.M, KeyCode.N, KeyCode.O, KeyCode.P, KeyCode.Q, KeyCode.R, KeyCode.S, KeyCode.T, KeyCode.V,
            KeyCode.W, KeyCode.X, KeyCode.Y, KeyCode.Z);

    public PasswordArea() 
        this.setEditable(false);
        this.setOnKeyPressed(event -> 
            if (!allowedKeys.contains(event.getCode())) 
                return;
            
            KeyCombination ctrlDelete = new KeyCodeCombination(KeyCode.BACK_SPACE, KeyCombination.CONTROL_DOWN);
            if(ctrlDelete.match(event)) 
                setPasswordText(getPasswordText());
                return;
            
            switch (event.getCode()) 
                case ENTER:
                    this.appendText("\n");
                    text.add("\n");
                    break;
                case SPACE:
                    this.appendText(" ");
                    text.add(" ");
                    break;
                case BACK_SPACE:
                    final int size = this.textProperty().length().get();
                    if (size > 0) 
                        this.deleteText(size - 1, size);
                        text.remove(text.size() - 1);
                    
                    break;
                default:
                    this.appendText("" + '\u2022');
                    text.add(event.getText());
                    break;
            
        );
    

    public String getPasswordText() 
        StringBuilder builder = new StringBuilder();
        text.forEach(builder::append);
        return builder.toString();
    

    public void setPasswordText(String setText) 
        text.clear();
        this.clear();
        for (int i = 0; i < setText.length(); i++) 
            switch (setText.charAt(i)) 
                case ' ':
                    this.appendText(" ");
                    break;
                case '\n':
                    this.appendText("\n");
                    break;
                default:
                    this.appendText("" + '\u2022');
                    break;
            
        
        text.add(setText);
    

用法:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;


public class Launch extends Application 

    public static void main(String[] args) 
        launch(args);
    

    @Override
    public void start(Stage primaryStage) throws Exception 
        PasswordArea area = new PasswordArea();
        Scene scene = new Scene(area, 600, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    


这就是我得到的,可能是最便宜的方法。

【讨论】:

在文本区域尝试getText() 会得到什么文本? 列表“文本”包含所有键 也许是我的错,我没有指定我希望getText()setText() 必须使用明文。就像PasswordField 一样。 哦,我不知道。我添加了 getPasswordText() 和 setPasswordText() 但这也可能不是你想要的。

以上是关于JavaFX - TextArea 的掩码文本的主要内容,如果未能解决你的问题,请参考以下文章

将点分式形式的掩码转换为十进制形式的掩码

“textarea”中的 JavaFX 8 计数行

JavaFX:TextArea 光标移回新文本的第一行

如何禁用 TextArea (JavaFX) 中的文本选择?

javafx 8在textarea中编辑文本

从 TextArea 拖动选定文本时出现 Javafx 问题