【问题标题】:Javafx slider value at mousemoved eventmousemoved 事件中的 Javafx 滑块值
【发布时间】:2015-12-02 06:33:28
【问题描述】:

我正在制作一个媒体播放器,并试图在将鼠标悬停在滑块上时获取光标位置的播放滑块值。为了尝试做到这一点,我使用了以下方法:

    timeSlider.addEventFilter(MouseEvent.MOUSE_MOVED, event -> System.out.println("hovering"));

只要鼠标在滑块上改变位置,就会打印“悬停”。谁能告诉我如何在当前光标位置获取滑块的值?我只能弄清楚如何获取拇指位置的值。

提前致谢。

【问题讨论】:

  • 有趣的是,似乎没有任何 api 可以将位置转换为值(反之亦然)
  • 所以也许需要更多信息......我想要实现两件事。 1)当我点击进度条(而不是拇指)时,随时知道光标下鼠标的值以调用 MediaPlayer 的 seek() 方法,因为我发现滑块在尝试寻找时相当无响应通过单击到新位置。 2)在光标位置显示时间,同时将鼠标悬停在工具提示气球之类的东西上,这样用户在点击进度条之前就会知道他们要寻找的时间。如果有更好的方法来实现这一结果,我会很高兴听到这些。

标签: java javafx-8 uicontrol


【解决方案1】:

如果您在滑块下方显示轴,这里有一点(可能不止一点)的技巧。它依赖于通过其 css 类查找轴,将鼠标坐标转换为相对于轴的坐标,然后使用来自ValueAxis 的 API 转换为值:

import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
import javafx.stage.Stage;

public class TooltipOnSlider extends Application {

    @Override
    public void start(Stage primaryStage) {
        Slider slider = new Slider(5, 25, 15);
        slider.setShowTickMarks(true);
        slider.setShowTickLabels(true);
        slider.setMajorTickUnit(5);

        Label label = new Label();
        Popup popup = new Popup();
        popup.getContent().add(label);

        double offset = 10 ;

        slider.setOnMouseMoved(e -> {
            NumberAxis axis = (NumberAxis) slider.lookup(".axis");
            Point2D locationInAxis = axis.sceneToLocal(e.getSceneX(), e.getSceneY());
            double mouseX = locationInAxis.getX() ;
            double value = axis.getValueForDisplay(mouseX).doubleValue() ;
            if (value >= slider.getMin() && value <= slider.getMax()) {
                label.setText(String.format("Value: %.1f", value));
            } else {
                label.setText("Value: ---");
            }
            popup.setAnchorX(e.getScreenX());
            popup.setAnchorY(e.getScreenY() + offset);
        });

        slider.setOnMouseEntered(e -> popup.show(slider, e.getScreenX(), e.getScreenY() + offset));
        slider.setOnMouseExited(e -> popup.hide());

        StackPane root = new StackPane(slider);
        primaryStage.setScene(new Scene(root, 350, 80));
        primaryStage.show();

    }

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

【讨论】:

  • 好主意 - 虽然只有在显示至少一个刻度或标签时才有效。可惜他们内部根本不使用轴(及其转换 api)
  • 我尝试使用轨道而不是轴,但似乎轨道偏移了一些任意量。也许如果 OP 的总体意图是帮助用户定位滑块上的特定时间点,那么轴无论如何都不是一件坏事。
  • 也许我不够清楚:使用轴是正确的事情,IMO :-)
  • 将我的 cmets 移动到一个答案中......好吧......然后再次删除它,坐标仍然错误:-(
  • 感谢 James_D,但除了滑块之前的当前位置和滑块之后的媒体总长度之外,我不想在滑块上显示任何内容。你能用上面提到的轨道给我看代码吗?我也许能够解决偏移问题。
【解决方案2】:

这主要是一个错误追踪:詹姆斯的答案是完美的 - 仅受到 2 个问题的阻碍:

    1234563无论如何)
  1. SliderSkin 中的一个错误会导致轴值与滑块值略有偏差。

要查看后者,这里是 James 代码的一个细微变化。要查看异步性,请将鼠标移到滑块上,然后单击。我们希望弹出窗口的值与滑块的值相同(显示在底部的标签中)。对于核心 SliderSkin,它们略有不同。

public class TooltipOnSlider extends Application {

    private boolean useAxis;
    @Override
    public void start(Stage primaryStage) {
        Slider slider = new Slider(5, 25, 15);
        useAxis = true;
        // force an axis to be used
        slider.setShowTickMarks(true);
        slider.setShowTickLabels(true);
        slider.setMajorTickUnit(5);

        // slider.setOrientation(Orientation.VERTICAL);
        // hacking around the bugs in a custom skin
        //  slider.setSkin(new MySliderSkin(slider));
        //  slider.setSkin(new XSliderSkin(slider));

        Label label = new Label();
        Popup popup = new Popup();
        popup.getContent().add(label);

        double offset = 30 ;

        slider.setOnMouseMoved(e -> {
            NumberAxis axis = (NumberAxis) slider.lookup(".axis");
            StackPane track = (StackPane) slider.lookup(".track");
            StackPane thumb = (StackPane) slider.lookup(".thumb");
            if (useAxis) {
                // James: use axis to convert value/position
                Point2D locationInAxis = axis.sceneToLocal(e.getSceneX(), e.getSceneY());
                boolean isHorizontal = slider.getOrientation() == Orientation.HORIZONTAL;
                double mouseX = isHorizontal ? locationInAxis.getX() : locationInAxis.getY() ;
                double value = axis.getValueForDisplay(mouseX).doubleValue() ;
                if (value >= slider.getMin() && value <= slider.getMax()) {
                    label.setText("" + value);
                } else {
                    label.setText("Value: ---");
                }

            } else {
                // this can't work because we don't know the internals of the track
                Point2D locationInAxis = track.sceneToLocal(e.getSceneX(), e.getSceneY());
                double mouseX = locationInAxis.getX();
                double trackLength = track.getWidth();
                double percent = mouseX / trackLength;
                double value = slider.getMin() + ((slider.getMax() - slider.getMin()) * percent);
                if (value >= slider.getMin() && value <= slider.getMax()) {
                    label.setText("" + value);
                } else {
                    label.setText("Value: ---");
                }
            }
            popup.setAnchorX(e.getScreenX());
            popup.setAnchorY(e.getScreenY() + offset);
        });

        slider.setOnMouseEntered(e -> popup.show(slider, e.getScreenX(), e.getScreenY() + offset));
        slider.setOnMouseExited(e -> popup.hide());

        Label valueLabel = new Label("empty");
        valueLabel.textProperty().bind(slider.valueProperty().asString());
        BorderPane root = new BorderPane(slider);
        root.setBottom(valueLabel);
        primaryStage.setScene(new Scene(root, 350, 100));
        primaryStage.show();
        primaryStage.setTitle("useAxis: " + useAxis + " mySkin: " + slider.getSkin().getClass().getSimpleName());
    }

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

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger.getLogger(TooltipOnSlider.class
            .getName());
}

请注意,有一个 open issue 报告了类似的行为(虽然不那么容易看到)

查看 SliderSkin 的代码,罪魁祸首似乎是轨道上鼠标事件的相对值计算不正确:

track.setOnMousePressed(me -> {
    ... 
    double relPosition = (me.getX() / trackLength);
    getBehavior().trackPress(me, relPosition);
    ...
});

轨道在滑块中的位置为:

// layout track 
track.resizeRelocate((int)(trackStart - trackRadius),
                     trackTop ,
                     (int)(trackLength + trackRadius + trackRadius),
                     trackHeight);

请注意,轨道的活动宽度(又名:trackLenght)会被 trackRadius 偏移,因此使用轨道上的原始 mousePosition 计算相对距离会产生轻微误差。

下面是一个粗略的自定义皮肤,它替换了 calc 只是为了测试小应用程序是否按预期运行。看起来很糟糕,因为需要使用反射来访问 super 的字段/方法,但现在滑块和轴值是同步的。

快速破解:

/**
 * Trying to work around down to the slight offset.
 */
public static class MySliderSkin extends SliderSkin {

    /**
     * Hook for replacing the mouse pressed handler that's installed by super.
     */
    protected void installListeners() {
        StackPane track = (StackPane) getSkinnable().lookup(".track");
        track.setOnMousePressed(me -> {
            invokeSetField("trackClicked", true);
            double trackLength = invokeGetField("trackLength");
            double trackStart = invokeGetField("trackStart");
            // convert coordinates into slider
            MouseEvent e = me.copyFor(getSkinnable(), getSkinnable());
            double mouseX = e.getX(); 
            double position;
            if (mouseX < trackStart) {
                position = 0;
            } else if (mouseX > trackStart + trackLength) {
                position = 1;
            } else {
               position = (mouseX - trackStart) / trackLength;
            }
            getBehavior().trackPress(e, position);
            invokeSetField("trackClicked", false);
        });
    }

    private double invokeGetField(String name) {
        Class clazz = SliderSkin.class;
        Field field;
        try {
            field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            return field.getDouble(this);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return 0.;
    }

    private void invokeSetField(String name, Object value) {
        Class clazz = SliderSkin.class;
        try {
            Field field = clazz.getDeclaredField(name);
            field.setAccessible(true);
            field.set(this, value);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    /**
     * Constructor - replaces listener on track.
     * @param slider
     */
    public MySliderSkin(Slider slider) {
        super(slider);
        installListeners();
    }

}

更深层次的解决方法可能是将所有脏坐标/值转换委托给轴 - 这就是它的设计目的。这要求轴始终是场景图的一部分,并且仅通过显示刻度/标签来切换其可见性。 first experiment 看起来很有希望。

【讨论】:

    猜你喜欢
    • 2020-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多