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();
}
}
當然,估計使用正則可能會有更簡單的實現方式。有時間我再研究一下。
J3001.JavaFX組件擴展(一)——IntegerField、DecimalField和CurrencyField