J3001.JavaFX组件扩展(一)——IntegerField、DecimalField和CurrencyField

我们在处理界面展现时,对于整型、浮点型、金额类型的数据时,希望界面组件至少已经处理了以下事项:

1、不接受非法输入。如对于整型来说,只能输入数字、负号,并且不允许超过当前平台上整形数值的最大值。

2、使用千分位对输入的数据进行格式化。

3、如果是货币型,则获取当前所在区域的货币符号等信息,并据此进行数据格式化。

对于界面处理人员来说,这是对开发组件库最基本的要求。但是实际上,JavaFX没有提供这些或相似的组件。开源组件中也没有找到类似的组件。著名的ControlsFX组件库的开发者额外提供了一个MoneyField,但功能比较弱,并且支持也不算好。

使用Java做界面,你得有这样一种觉悟:JDK提供的界面功能,有时候是比较弱的,甚至是比较弱智的,得自行不断丰富、完善自己的组件库。有时可以采用一些第三方的类库,有时一些特别基础的组件,也得自己写。可能Oracle认为每一个Java界面开发者首先都是合格的界面组件开发人员。

想写这三个组件很久了,一直没有时间。昨天我下班后,坐在新电脑前,发了一会儿呆后,花了几个小时实现了这三个组件。今天上班前,测试完善了一下,感觉可以放出来,供大家参考了。

先看一下效果:

有三个输入框,分别只接受Integer、Decimal(4位小数)、Currency类型的数据。并提供两个按钮,一个是打印数值,一个是通过setText()的方式给Decimal赋值。

直接放代码。

NumberTypeEnum枚举类,用于区分不同类型的输入数据,以便于基类分别处理。

package com.lirong.foundation.ui.javafx.control;

/**
 * <p>Title: LiRong Java Application Platform</p>
 * Description: AbstractNumberField及其子类支持的数据类型 <br>
 * Copyright: CorpRights lrjap.com<br>
 * Company: lrjap.com<br>
 *
 * @author yujj
 * @version 1.1.1
 * @date 2017-12-27
 */
public enum NumberTypeEnum {
    INTEGER,
    CURRENCY,
    DECIMAL;
}

AbstractNumberField基类,封装了大多数行为,这是最重要的一个类。

package com.lirong.foundation.ui.javafx.control;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import org.apache.commons.lang3.StringUtils;

/**
 * <p>Title: LiRong Java Application Platform</p>
 * Description: 整数、高精度浮点数、货币 数值输入框的虚拟基类,自动校验输入合法性,自动增加货币符号、千分位 <br>
 * Copyright: CorpRights lrjap.com<br>
 * Company: lrjap.com<br>
 *
 * @author yujj
 * @version 1.1.1
 * @date 2017-12-27
 */
public abstract class AbstractNumberField extends TextField {

    private NumberTypeEnum numberType;

    private static final String DEFAULT_NUMBER_SEPARTOR_FORMAT = ",###";

    private final static DecimalFormatSymbols symbols = new DecimalFormatSymbols();

    public AbstractNumberField(NumberTypeEnum numberType) {

        super();
        this.numberType = numberType;
        setAlignment(Pos.CENTER_RIGHT);
        // 输入时的有效性检查
        addEventFilter(KeyEvent.KEY_TYPED, event -> {

            if (!isValid(getText())) {
                event.consume();
            }
        });
        // 格式化
        textProperty().addListener((observableValue, oldValue, newValue) -> {

            if (!isValid(newValue)) {
                setText(oldValue);
            }
            setText(formatValue(getFormatter()));
        });
    }

    /**
     * 格式化数值
     *
     * @param valueFormatter 格式
     * @return
     */
    private String formatValue(final String valueFormatter) {

        if ("-".equals(getText())) {
            return getText();
        }
        String currString = null;
        if (StringUtils.isNotBlank(getText())) {
            if (getText().endsWith(".") || getText().endsWith(getCurrencySymbols())) {
                return getText();
            }
            DecimalFormat numberFormatter = new DecimalFormat(valueFormatter);
            if (NumberTypeEnum.INTEGER == this.numberType) {
                Integer currValue = getIntegerValue();
                currString = numberFormatter.format(currValue);
            } else {
                BigDecimal currValue = getDecimalValue();
                currString = numberFormatter.format(currValue);
            }
        }
        return currString;
    }

    /**
     * 数值有效性检查
     *
     * @param value 带格式的字符串
     * @return
     */
    private boolean isValid(final String value) {

        if (StringUtils.isBlank(value) || value.equals("-")) {
            return true;
        }

        try {
            if (NumberTypeEnum.INTEGER == this.numberType) {
                getIntegerValue();
            } else if (NumberTypeEnum.CURRENCY == this.numberType) {
                getDecimalValue();
            } else {
                getCurrencyValue();
            }
            return true;
        } catch (NumberFormatException ex) {
            return false;
        }
    }

    /**
     * 转为整型
     *
     * @return
     */
    protected Integer getIntegerValue() {

        if (StringUtils.isBlank(getText()) || "-".equals(getText())) {
            return null;
        }
        return Integer.valueOf(getText().replace(",", ""));
    }

    /**
     * 转为BigDecimal
     *
     * @return
     */
    protected BigDecimal getDecimalValue() {

        return getDecimalValue('.');
    }

    /**
     * 转为货币
     *
     * @return
     */
    protected BigDecimal getCurrencyValue() {

        return getDecimalValue(getCurrencySeparator());
    }

    private BigDecimal getDecimalValue(final char separator) {

        if (StringUtils.isBlank(getText()) || "-".equals(getText())) {
            return null;
        }
        int pos = getText().indexOf(separator);
        if (pos > -1) {
            final String subStr = getText().substring(pos + 1, getText().length());
            if (subStr.length() > decimalScale()) {
                throw new NumberFormatException("Scale error.");
            }
        }
        return new BigDecimal(getText().replace(",", "").replace(getCurrencySymbols(), ""));
    }

    /**
     * 生成用于格式化数据的字符串
     *
     * @return
     */
    protected String getFormatter() {

        if (this.numberType == null) {
            throw new RuntimeException("Type error.");
        }
        if (NumberTypeEnum.INTEGER == this.numberType) {
            return getIntegerFormatter();
        } else if (NumberTypeEnum.CURRENCY == this.numberType) {
            return getCurrencyFormatter();
        } else {
            return getDecimalFormatter();
        }
    }

    protected String getIntegerFormatter() {

        return DEFAULT_NUMBER_SEPARTOR_FORMAT;
    }

    protected String getCurrencyFormatter() {

        return String.format("%s%s%s", getCurrencySymbols(), DEFAULT_NUMBER_SEPARTOR_FORMAT, getScaleFormatter());
    }

    protected String getDecimalFormatter() {

        return String.format("%s%s", DEFAULT_NUMBER_SEPARTOR_FORMAT, getScaleFormatter());
    }

    public abstract Integer decimalScale();

    /**
     * 为BigDecimal和货币型数据生成小数占位信息,有多少有效小数位就生成多少个占位符
     *
     * @return
     */
    protected String getScaleFormatter() {

        String currFormatter = "";
        if (decimalScale() == 0) {
            return currFormatter;
        } else {
            if (NumberTypeEnum.CURRENCY == this.numberType) {
                currFormatter += getCurrencySeparator();
            } else {
                currFormatter += ".";
            }
            Integer tempScale = decimalScale();
            while (tempScale > 0) {
                currFormatter += "#";
                tempScale--;
            }
            return currFormatter;
        }
    }

    /**
     * 获取货币符号
     *
     * @return
     */
    protected static String getCurrencySymbols() {

        return symbols.getCurrencySymbol();
    }

    /**
     * 获取货币分隔符
     *
     * @return
     */
    protected static char getCurrencySeparator() {

        return symbols.getMonetaryDecimalSeparator();
    }

    /**
     * 虚拟方法。用于子类返回指定类型的数值
     *
     * @return
     */
    public abstract Object getValue();
}

以下分别是三个实现类,主要是对小数位数、返回值的类型进行处理。

CurrencyField:

package com.lirong.foundation.ui.javafx.control;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Locale;

/**
 * <p>Title: LiRong Java Application Platform</p>
 * Description: <br>
 * Copyright: CorpRights lrjap.com<br>
 * Company: lrjap.com<br>
 *
 * @author yujj
 * @version 1.1.1
 * @date 2017-12-27
 */
public class CurrencyField extends AbstractNumberField {

    public CurrencyField() {

        super(NumberTypeEnum.CURRENCY);
    }

    @Override
    public Integer decimalScale() {

        Locale locale = Locale.getDefault();
        DecimalFormat formatter = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
        return formatter.getCurrency().getDefaultFractionDigits();
    }

    @Override
    public BigDecimal getValue() {

        return getCurrencyValue();
    }
}

DecimalField:

package com.lirong.foundation.ui.javafx.control;

import java.math.BigDecimal;

/**
 * <p>Title: LiRong Java Application Platform</p>
 * Description: <br>
 * Copyright: CorpRights lrjap.com<br>
 * Company: lrjap.com<br>
 *
 * @author yujj
 * @version 1.1.1
 * @date 2017-12-27
 */
public class DecimalField extends AbstractNumberField {

    private Integer scale = 2;

    public DecimalField() {

        super(NumberTypeEnum.DECIMAL);
    }

    public DecimalField(final Integer scale) {

        this();
        if (scale < 0) {
            throw new NumberFormatException("Scale must great than equals to 0.");
        }
        this.scale = scale;
    }

    @Override
    public Integer decimalScale() {

        return this.scale;
    }

    @Override
    public BigDecimal getValue() {

        return getDecimalValue();
    }
}

IntegerField:

package com.lirong.foundation.ui.javafx.control;

/**
 * <p>Title: LiRong Java Application Platform</p>
 * Description: <br>
 * Copyright: CorpRights lrjap.com<br>
 * Company: lrjap.com<br>
 *
 * @author yujj
 * @version 1.1.1
 * @date 2017-12-27
 */
public class IntegerField extends AbstractNumberField {

    public IntegerField() {

        super(NumberTypeEnum.INTEGER);
    }

    @Override
    public Integer decimalScale() {

        return Integer.valueOf(0);
    }

    @Override
    public Integer getValue() {

        return getIntegerValue();
    }
}

测试类:

package com.lirong.test.ui.javafx.decimalfiled;

import com.lirong.foundation.ui.javafx.control.CurrencyField;
import com.lirong.foundation.ui.javafx.control.DecimalField;
import com.lirong.foundation.ui.javafx.control.IntegerField;
import java.math.BigDecimal;
import javafx.application.Application;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.*;
import javafx.stage.Stage;

/**
 * <p>Title: LiRong Java Application Platform</p>
 * Description: <br>
 * Copyright: CorpRights lrjap.com<br>
 * Company: lrjap.com<br>
 *
 * @author yujj
 * @version 1.1.1
 * @date 2017-12-27
 */
public class TestNumberField extends Application {

    public static void main(String[] args) {

        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        ColumnConstraints columnLabel = new ColumnConstraints();
        columnLabel.setPrefWidth(120);
        columnLabel.setHalignment(HPos.RIGHT);

        ColumnConstraints columnControll = new ColumnConstraints();
        columnControll.setHgrow(Priority.ALWAYS);

        GridPane gridPane = new GridPane();
        gridPane.setPadding(new Insets(10));
        gridPane.setHgap(10);
        gridPane.setVgap(10);
        gridPane.getColumnConstraints().addAll(columnLabel, columnControll);

        Label lblInteger = new Label("Integer:");
        IntegerField integerTextField = new IntegerField();
        integerTextField.setPromptText("Please input a integer value");
        gridPane.add(lblInteger, 0, 0);
        gridPane.add(integerTextField, 1, 0);

        Label lblDecimal = new Label("Decimal:");
        DecimalField decimalField = new DecimalField(4);
        decimalField.setPromptText("Please input a decimal value");
        gridPane.add(lblDecimal, 0, 1);
        gridPane.add(decimalField, 1, 1);

        Label lblCurrency = new Label("Currency:");
        CurrencyField currencyField = new CurrencyField();
        currencyField.setPromptText("Please input a currency value");
        gridPane.add(lblCurrency, 0, 2);
        gridPane.add(currencyField, 1, 2);

        TextArea textConsole = new TextArea();
        textConsole.setEditable(Boolean.FALSE);
        gridPane.add(textConsole, 0, 6, 2, 6);

        HBox toolBar = new HBox();
        toolBar.setPadding(new Insets(10));
        toolBar.setSpacing(10);

        Button buttonPrintValue = new Button("PrintValue");
        buttonPrintValue.setMinSize(75, 30);
        buttonPrintValue.setOnAction(action -> {

            final String LINE_SEP = System.getProperty("line.separator");

            StringBuilder sbInfo = new StringBuilder();
            sbInfo.append(String.format("%s=%s", "IntegerField Value", integerTextField.getValue())).append(LINE_SEP).append(LINE_SEP);
            sbInfo.append(String.format("%s=%s", "DecimalField Value", decimalField.getValue())).append(LINE_SEP).append(LINE_SEP);
            sbInfo.append(String.format("%s=%s", "CurrencyField Value", currencyField.getValue())).append(LINE_SEP);
            textConsole.setText(sbInfo.toString());
        });

        Button buttonSetValue = new Button("SetValue");
        buttonSetValue.setMinSize(75, 30);
        buttonSetValue.setOnAction(action -> decimalField.setText(new BigDecimal("2080280808.2223").toString()));

        toolBar.getChildren().addAll(buttonPrintValue, buttonSetValue);
        gridPane.add(toolBar, 0, 4, 2, 2);

        BorderPane container = new BorderPane();
        container.setCenter(gridPane);

        Scene scene = new Scene(container, 600, 300);
        primaryStage.setTitle("IntegerField、DecimalField、CurrencyField 测试");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

当然,估计使用正则可能会有更简单的实现方式。有时间我再研究一下。

posted @ 2018-05-01 19:54  yujj_cn  阅读(366)  评论(0编辑  收藏  举报