这主要是一个错误追踪:詹姆斯的答案是完美的 - 仅受到 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 看起来很有希望。