译自: http://java.dzone.com/articles/javafx-numbertextfield-and
作者: Thomas Bolz
我最近花了一些时间学习javaFX, 要更深入地理解新GUI包, 自定义控制器可能是一个比较好的方法. 由于我是开发财务软件的, 所以我当然希望javaFX中也有类似JFormattedTextField和JSpinner的控件. 这对我来说确实是个不错的选择.
这是我的控制器:
- 数字文本框(NumberTextField): 可以配置任意格式的数字;
- 微调控制器( NumberSpinner ): 可以使用键盘方向键或箭头按钮来控制数值;它也是控制器的一部分;
控制器及其示例可以在这里下载(可直接导入到netbeans,见附件). 示例中还包含一个css样式文件, 它用于控制Spinner的风格是直角或圆角.
NumberTextField
NumberTextField 的实现很容易,以致我认为这算不上自定义控制器, 而仅仅是改变一个已存在的控制器的一些行为而已.
NumberTextField
扩展自JFX中的文本框(TextField), 添加一个
使用BigDecimal(由于财务软件需要精确的类型)的
NumberProperty作为模型, 并做一些格式化和解析处理. 就这样, 不复杂.
package de.thomasbolz.javafx;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.text.ParseException;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.TextField;
/**
* Textfield implementation that accepts formatted number and stores them in a
* BigDecimal property The user input is formatted when the focus is lost or the
* user hits RETURN.
*
* @author Thomas Bolz
*/
public class NumberTextField extends TextField {
private final NumberFormat nf;
private ObjectProperty<BigDecimal> number = new SimpleObjectProperty<>();
public final BigDecimal getNumber() {
return number.get();
}
public final void setNumber(BigDecimal value) {
number.set(value);
}
public ObjectProperty<BigDecimal> numberProperty() {
return number;
}
public NumberTextField() {
this(BigDecimal.ZERO);
}
public NumberTextField(BigDecimal value) {
this(value, NumberFormat.getInstance());
}
public NumberTextField(BigDecimal value, NumberFormat nf) {
super();
this.nf = nf;
initHandlers();
setNumber(value);
}
private void initHandlers() {
// try to parse when focus is lost or RETURN is hit
setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent arg0) {
parseAndFormatInput();
}
});
focusedProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (!newValue.booleanValue()) {
parseAndFormatInput();
}
}
});
// Set text in field if BigDecimal property is changed from outside.
numberProperty().addListener(new ChangeListener<BigDecimal>() {
@Override
public void changed(ObservableValue<? extends BigDecimal> obserable, BigDecimal oldValue, BigDecimal newValue) {
setText(nf.format(newValue));
}
});
}
/**
* Tries to parse the user input to a number according to the provided
* NumberFormat
*/
private void parseAndFormatInput() {
try {
String input = getText();
if (input == null || input.length() == 0) {
return;
}
Number parsedNumber = nf.parse(input);
BigDecimal newValue = new BigDecimal(parsedNumber.toString());
setNumber(newValue);
selectAll();
} catch (ParseException ex) {
// If parsing fails keep old number
setText(nf.format(number.get()));
}
}
}
NumberSpinner
NumberSpinner好像复杂一点. 它构建在NumberTextField 上, 并使用递增和递减按钮来改变文本框中数字的值, 每次改变步长为stepwidth.
stepwidth和NumberFormat的初始值在构造器中指定. 文本框和按钮的大小取决于文本的大小. 文本的大小可在.css文件中设置.
package de.thomasbolz.javafx;
import java.math.BigDecimal;
import java.text.NumberFormat;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javax.swing.JSpinner;
/**
* JavaFX Control that behaves like a {@link JSpinner} known in Swing. The
* number in the textfield can be incremented or decremented by a configurable
* stepWidth using the arrow buttons in the control or the up and down arrow
* keys.
*
* @author Thomas Bolz
*/
public class NumberSpinner extends HBox {
public static final String ARROW = "NumberSpinnerArrow";
public static final String NUMBER_FIELD = "NumberField";
public static final String NUMBER_SPINNER = "NumberSpinner";
public static final String SPINNER_BUTTON_UP = "SpinnerButtonUp";
public static final String SPINNER_BUTTON_DOWN = "SpinnerButtonDown";
private final String BUTTONS_BOX = "ButtonsBox";
private NumberTextField numberField;
private ObjectProperty<BigDecimal> stepWitdhProperty = new SimpleObjectProperty<>();
private final double ARROW_SIZE = 4;
private final Button incrementButton;
private final Button decrementButton;
private final NumberBinding buttonHeight;
private final NumberBinding spacing;
public NumberSpinner() {
this(BigDecimal.ZERO, BigDecimal.ONE);
}
public NumberSpinner(BigDecimal value, BigDecimal stepWidth) {
this(value, stepWidth, NumberFormat.getInstance());
}
public NumberSpinner(BigDecimal value, BigDecimal stepWidth, NumberFormat nf) {
super();
this.setId(NUMBER_SPINNER);
this.stepWitdhProperty.set(stepWidth);
// TextField
numberField = new NumberTextField(value, nf);
numberField.setId(NUMBER_FIELD);
// Enable arrow keys for dec/inc
numberField.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.DOWN) {
decrement();
keyEvent.consume();
}
if (keyEvent.getCode() == KeyCode.UP) {
increment();
keyEvent.consume();
}
}
});
// Painting the up and down arrows
Path arrowUp = new Path();
arrowUp.setId(ARROW);
arrowUp.getElements().addAll(new MoveTo(-ARROW_SIZE, 0), new LineTo(ARROW_SIZE, 0),
new LineTo(0, -ARROW_SIZE), new LineTo(-ARROW_SIZE, 0));
// mouse clicks should be forwarded to the underlying button
arrowUp.setMouseTransparent(true);
Path arrowDown = new Path();
arrowDown.setId(ARROW);
arrowDown.getElements().addAll(new MoveTo(-ARROW_SIZE, 0), new LineTo(ARROW_SIZE, 0),
new LineTo(0, ARROW_SIZE), new LineTo(-ARROW_SIZE, 0));
arrowDown.setMouseTransparent(true);
// the spinner buttons scale with the textfield size
// TODO: the following approach leads to the desired result, but it is
// not fully understood why and obviously it is not quite elegant
buttonHeight = numberField.heightProperty().subtract(3).divide(2);
// give unused space in the buttons VBox to the incrementBUtton
spacing = numberField.heightProperty().subtract(2).subtract(buttonHeight.multiply(2));
// inc/dec buttons
VBox buttons = new VBox();
buttons.setId(BUTTONS_BOX);
incrementButton = new Button();
incrementButton.setId(SPINNER_BUTTON_UP);
incrementButton.prefWidthProperty().bind(numberField.heightProperty());
incrementButton.minWidthProperty().bind(numberField.heightProperty());
incrementButton.maxHeightProperty().bind(buttonHeight.add(spacing));
incrementButton.prefHeightProperty().bind(buttonHeight.add(spacing));
incrementButton.minHeightProperty().bind(buttonHeight.add(spacing));
incrementButton.setFocusTraversable(false);
incrementButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent ae) {
increment();
ae.consume();
}
});
// Paint arrow path on button using a StackPane
StackPane incPane = new StackPane();
incPane.getChildren().addAll(incrementButton, arrowUp);
incPane.setAlignment(Pos.CENTER);
decrementButton = new Button();
decrementButton.setId(SPINNER_BUTTON_DOWN);
decrementButton.prefWidthProperty().bind(numberField.heightProperty());
decrementButton.minWidthProperty().bind(numberField.heightProperty());
decrementButton.maxHeightProperty().bind(buttonHeight);
decrementButton.prefHeightProperty().bind(buttonHeight);
decrementButton.minHeightProperty().bind(buttonHeight);
decrementButton.setFocusTraversable(false);
decrementButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent ae) {
decrement();
ae.consume();
}
});
StackPane decPane = new StackPane();
decPane.getChildren().addAll(decrementButton, arrowDown);
decPane.setAlignment(Pos.CENTER);
buttons.getChildren().addAll(incPane, decPane);
this.getChildren().addAll(numberField, buttons);
}
/**
* increment number value by stepWidth
*/
private void increment() {
BigDecimal value = numberField.getNumber();
value = value.add(stepWitdhProperty.get());
numberField.setNumber(value);
}
/**
* decrement number value by stepWidth
*/
private void decrement() {
BigDecimal value = numberField.getNumber();
value = value.subtract(stepWitdhProperty.get());
numberField.setNumber(value);
}
public final void setNumber(BigDecimal value) {
numberField.setNumber(value);
}
public ObjectProperty<BigDecimal> numberProperty() {
return numberField.numberProperty();
}
public final BigDecimal getNumber() {
return numberField.getNumber();
}
// debugging layout bounds
public void dumpSizes() {
System.out.println("numberField (layout)=" + numberField.getLayoutBounds());
System.out.println("buttonInc (layout)=" + incrementButton.getLayoutBounds());
System.out.println("buttonDec (layout)=" + decrementButton.getLayoutBounds());
System.out.println("binding=" + buttonHeight.toString());
System.out.println("spacing=" + spacing.toString());
}
}
number_spinner.css
最后, 控制器的样式可在css文件中设置. 我实现了圆角和直角两种风格(见上文截图). 你可以通过修改 #NumberField, #ButtonBox, #SpinnerButtonUp 和#SpinnerButtonDown 中的border/background-radiuses来切换不同的风格.
.root{
-fx-font-size: 24pt;
/* -fx-base: rgb(255,0,0);*/
/* -fx-background: rgb(50,50,50);*/
}
#NumberField {
-fx-border-width: 1;
-fx-border-color: lightgray;
-fx-background-insets:1;
-fx-border-radius:3 0 0 3;
/* -fx-border-radius:0 0 0 0;*/
}
#NumberSpinnerArrow {
-fx-fill: gray;
-fx-stroke: gray;
/* -fx-effect: innershadow( gaussian , black , 2 , 0.6 , 1 , 1 )*/
}
#ButtonsBox {
-fx-border-color:lightgray;
-fx-border-width: 1 1 1 0;
-fx-border-radius: 0 3 3 0;
/* -fx-border-radius: 0 0 0 0;*/
}
#SpinnerButtonUp {
-fx-background-insets: 0;
-fx-background-radius:0 3 0 0;
/* -fx-background-radius:0;*/
}
#SpinnerButtonDown {
-fx-background-insets: 0;
-fx-background-radius:0 0 3 0;
/* -fx-background-radius:0;*/
}
结论
从上面的例子可以看出在javaFx中自定义控制器并不困难. JavaFX自2.0版本以后作为一个纯粹的java API, 其比以前的任何版本更好地整合诸如groovy(BigDecimal的乐土)的语言. 这将是财务桌面应用的黄金组合.