mousemoved 事件中的 Javafx 滑块值

Posted

技术标签:

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

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

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

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

提前致谢。

【问题讨论】:

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

    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 看起来很有希望。

【讨论】:

以上是关于mousemoved 事件中的 Javafx 滑块值的主要内容,如果未能解决你的问题,请参考以下文章

JavaFX:隐藏SplitPane的滑块/分隔线

JavaFX - 更改特定于滑块的颜色

MouseEnter/MouseMove 面板,包括空格

如何在 React 中处理父级的 mouseMove 事件?

JavaFX UI 在事件侦听器中的 JavaFX 应用程序线程中冻结,但可与 Platform.runLater 一起使用

javaFx中的事件处理程序用于菜单