`

javafx2 : 支持使用微调(spinner)控制的数字的文本框(NemberTextField)

 
阅读更多

译自: http://java.dzone.com/articles/javafx-numbertextfield-and

作者: Thomas Bolz

 

 

我最近花了一些时间学习javaFX, 要更深入地理解新GUI包, 自定义控制器可能是一个比较好的方法. 由于我是开发财务软件的, 所以我当然希望javaFX中也有类似JFormattedTextField和JSpinner的控件. 这对我来说确实是个不错的选择.

这是我的控制器:

 

  • 数字文本框(NumberTextField): 可以配置任意格式的数字;
  • 微调控制器( NumberSpinner ): 可以使用键盘方向键或箭头按钮来控制数值;它也是控制器的一部分;

 


控制器及其示例可以在这里下载(可直接导入到netbeans,见附件).  示例中还包含一个css样式文件,  它用于控制Spinner的风格是直角或圆角.


spinner with rounded corners

 

 spinner with straight corners

 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的乐土)的语言. 这将是财务桌面应用的黄金组合.


1
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics