如何在JavaFX中创建一个时间选择器(TimeSpinner)?

6
我想制作一个类似这样的时间选择器:Youtube视频 如果有人知道源代码在哪里,那就太完美了。但是如果没有,我想尝试自己实现它,但是我该如何制作三个不同的(可聚焦的?)文本框像这样?
编辑:这是我得到的内容,但我想能够选择小时并递增小时,而不仅仅是分钟(当然秒钟也一样)。
Spinner<LocalTime> spinner = new Spinner(new SpinnerValueFactory() {

        {
            setConverter(new LocalTimeStringConverter(FormatStyle.MEDIUM));
        }

        @Override
        public void decrement(int steps) {
            if (getValue() == null)
                setValue(LocalTime.now());
            else {
                LocalTime time = (LocalTime) getValue();
                setValue(time.minusMinutes(steps));
            }
        }

        @Override
        public void increment(int steps) {
            if (this.getValue() == null)
                setValue(LocalTime.now());
            else {
                LocalTime time = (LocalTime) getValue();
                setValue(time.plusMinutes(steps));
            }
        }
    });
    spinner.setEditable(true);

这是我得到的结果:

在此输入图片描述

谢谢。

尝试实现它,如果遇到困难,请发布具体问题。就目前而言,您的问题不适合在此论坛讨论。 - James_D
我其实不知道从哪里开始... - Phoste
可能首先使用自定义的SpinnerValueFactory创建一个Spinner<LocalTime> - James_D
谢谢您的帮助。我已经编辑了我的帖子,附上了我刚刚所做的内容。但我仍然想知道如何选择这三个不同的数据 :) - Phoste
你可以像这样做:spinner.getEditor().getCaretPosition(),然后将其与分隔符(即 :)的位置进行比较,以确定要增加/减少哪个单位。 - James_D
3个回答

17
我认为选择编辑器的各个部分的最佳方法是检查编辑器中的 caretPosition并根据需要递增/递减相应的部分。您还可以在编辑器上设置 TextFormatter以控制允许的输入等。
以下是一个快速尝试:不旨在生产质量,但应该是一个良好的起点:
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.InputEvent;
import javafx.util.StringConverter;

public class TimeSpinner extends Spinner<LocalTime> {

    // Mode represents the unit that is currently being edited.
    // For convenience expose methods for incrementing and decrementing that
    // unit, and for selecting the appropriate portion in a spinner's editor
    enum Mode {

        HOURS {
           @Override
           LocalTime increment(LocalTime time, int steps) {
               return time.plusHours(steps);
           }
           @Override
           void select(TimeSpinner spinner) {
               int index = spinner.getEditor().getText().indexOf(':');
               spinner.getEditor().selectRange(0, index);
           }
        },
        MINUTES {
            @Override
            LocalTime increment(LocalTime time, int steps) {
                return time.plusMinutes(steps);
            }
            @Override
            void select(TimeSpinner spinner) {
                int hrIndex = spinner.getEditor().getText().indexOf(':');
                int minIndex = spinner.getEditor().getText().indexOf(':', hrIndex + 1);
                spinner.getEditor().selectRange(hrIndex+1, minIndex);
            }
        },
        SECONDS {
            @Override
            LocalTime increment(LocalTime time, int steps) {
                return time.plusSeconds(steps);
            }
            @Override
            void select(TimeSpinner spinner) {
                int index = spinner.getEditor().getText().lastIndexOf(':');
                spinner.getEditor().selectRange(index+1, spinner.getEditor().getText().length());
            }
        };
        abstract LocalTime increment(LocalTime time, int steps);
        abstract void select(TimeSpinner spinner);
        LocalTime decrement(LocalTime time, int steps) {
            return increment(time, -steps);
        }
    }

    // Property containing the current editing mode:

    private final ObjectProperty<Mode> mode = new SimpleObjectProperty<>(Mode.HOURS) ;

    public ObjectProperty<Mode> modeProperty() {
        return mode;
    }

    public final Mode getMode() {
        return modeProperty().get();
    }

    public final void setMode(Mode mode) {
        modeProperty().set(mode);
    }


    public TimeSpinner(LocalTime time) {
        setEditable(true);

        // Create a StringConverter for converting between the text in the
        // editor and the actual value:

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

        StringConverter<LocalTime> localTimeConverter = new StringConverter<LocalTime>() {

            @Override
            public String toString(LocalTime time) {
                return formatter.format(time);
            }

            @Override
            public LocalTime fromString(String string) {
                String[] tokens = string.split(":");
                int hours = getIntField(tokens, 0);
                int minutes = getIntField(tokens, 1) ;
                int seconds = getIntField(tokens, 2);
                int totalSeconds = (hours * 60 + minutes) * 60 + seconds ;
                return LocalTime.of((totalSeconds / 3600) % 24, (totalSeconds / 60) % 60, seconds % 60);
            }

            private int getIntField(String[] tokens, int index) {
                if (tokens.length <= index || tokens[index].isEmpty()) {
                    return 0 ;
                }
                return Integer.parseInt(tokens[index]);
            }

        };

        // The textFormatter both manages the text <-> LocalTime conversion,
        // and vetoes any edits that are not valid. We just make sure we have
        // two colons and only digits in between:

        TextFormatter<LocalTime> textFormatter = new TextFormatter<LocalTime>(localTimeConverter, LocalTime.now(), c -> {
            String newText = c.getControlNewText();
            if (newText.matches("[0-9]{0,2}:[0-9]{0,2}:[0-9]{0,2}")) {
                return c ;
            }
            return null ;
        });

        // The spinner value factory defines increment and decrement by
        // delegating to the current editing mode:

        SpinnerValueFactory<LocalTime> valueFactory = new SpinnerValueFactory<LocalTime>() {


            {

                setConverter(localTimeConverter);
                setValue(time);
            }

            @Override
            public void decrement(int steps) {
                setValue(mode.get().decrement(getValue(), steps));
                mode.get().select(TimeSpinner.this);
            }

            @Override
            public void increment(int steps) {
                setValue(mode.get().increment(getValue(), steps));
                mode.get().select(TimeSpinner.this);
            }

        };

        this.setValueFactory(valueFactory);
        this.getEditor().setTextFormatter(textFormatter);

        // Update the mode when the user interacts with the editor.
        // This is a bit of a hack, e.g. calling spinner.getEditor().positionCaret()
        // could result in incorrect state. Directly observing the caretPostion
        // didn't work well though; getting that to work properly might be
        // a better approach in the long run.
        this.getEditor().addEventHandler(InputEvent.ANY, e -> {
            int caretPos = this.getEditor().getCaretPosition();
            int hrIndex = this.getEditor().getText().indexOf(':');
            int minIndex = this.getEditor().getText().indexOf(':', hrIndex + 1);
            if (caretPos <= hrIndex) {
                mode.set( Mode.HOURS );
            } else if (caretPos <= minIndex) {
                mode.set( Mode.MINUTES );
            } else {
                mode.set( Mode.SECONDS );
            }
        });

        // When the mode changes, select the new portion:
        mode.addListener((obs, oldMode, newMode) -> newMode.select(this));

    }

    public TimeSpinner() {
        this(LocalTime.now());
    }
}

这里是使用它的快速示例:

import java.time.format.DateTimeFormatter;

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


public class TimeSpinnerExample extends Application  {
    @Override
    public void start(Stage primaryStage) {

        TimeSpinner spinner = new TimeSpinner();

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm:ss a");
        spinner.valueProperty().addListener((obs, oldTime, newTime) -> 
            System.out.println(formatter.format(newTime)));

        StackPane root = new StackPane(spinner);
        Scene scene = new Scene(root, 350, 120);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

非常感谢,这比我想象的要复杂。虽然我尝试了你的代码,但是当我尝试增加或减少微调器时,我遇到了NullPointerException。 - Phoste
我只是从我的工作示例中复制了它。你做了什么来获得NPE?它是哪一行? - James_D
在每个枚举模式的增量方法中添加代码。似乎微调器的值为空,我只是在构造函数中添加了setValue(time),现在它可以正常工作了 ;) - Phoste
感谢:已相应地更新了代码;我没有看到NPE,但无论如何它都应该被初始化为一个值。 - James_D
这应该是一个小型库 :) - NM Naufaldo

0

将您的转换器构造函数更改为:

    setConverter(new LocalTimeStringConverter(DateTimeFormatter.ofPattern("HH:mm"), DateTimeFormatter.ofPattern("HH:mm")));

enter image description here


0

由于我声望不够,无法发表评论,所以在这里对James_D的帖子做出简短回应:

我修复了一个小错误,该错误会在构造函数中指定自定义时间时出现。

//  wrong: LocalTime.now()
//  TextFormatter<LocalTime> textFormatter = new TextFormatter<LocalTime>(localTimeConverter, LocalTime.now(), c -> {

//  instead use variable "time"
    TextFormatter<LocalTime> textFormatter = new TextFormatter<LocalTime>(localTimeConverter, time, c -> {

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接