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 滑块值的主要内容,如果未能解决你的问题,请参考以下文章
如何在 React 中处理父级的 mouseMove 事件?
JavaFX UI 在事件侦听器中的 JavaFX 应用程序线程中冻结,但可与 Platform.runLater 一起使用