JavaFx Group的内容位置在转换后会发生变化 [英] JavaFx Group contents position getting changed upon transformation
问题描述
我正在模拟一个罗盘,在该罗盘上,我需要从另一个来源(通过网络)接收角度数据时显示当前角度位置.但是以某种方式,基于角位置将变换应用于文本"节点后,圆组会发生偏移.这是示例代码
I'm simulating a compass where I need to display current angular position upon reception of angular data from another source(via network). But somehow the circle group is getting shifted upon applying transformation to Text node based upon angular position. This is a sample code
MainApp.java
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class MainApp extends Application {
static BorderPane borderPane;
static Group clock;
static Group angStatus;
public static Circle clockCircle;
public static Line angHand;
public static Text angText;
public static Rotate angRotate;
public static Label angLabel;
public static void main(String[] args) {
borderPane = new BorderPane();
clock = new Group();
angStatus = new Group();
clockCircle = new Circle();
clockCircle.centerXProperty().bind(borderPane.layoutXProperty());
clockCircle.centerYProperty().bind(borderPane.layoutYProperty());
clockCircle.setRadius(360);
clockCircle.setFill(Color.RED);
angHand = new Line();
angHand.startXProperty().bind(clockCircle.centerXProperty());
angHand.startYProperty().bind(clockCircle.centerYProperty());
angText = new Text();
angRotate = new Rotate();
angRotate.pivotXProperty().bind(angText.xProperty());
angRotate.pivotYProperty().bind(angText.yProperty());
angText.getTransforms().addAll(angRotate);
angLabel = new Label();
angLabel.layoutXProperty().bind(borderPane.layoutXProperty());
angLabel.layoutYProperty().bind(borderPane.layoutYProperty());
clock.getChildren().addAll(clockCircle, angHand, angText);
angStatus.getChildren().addAll(angLabel);
borderPane.setCenter(clock);
borderPane.setBottom(angStatus);
DataReceiver objDataReceiver = new DataReceiver();
Thread dataRecvThread = new Thread(objDataReceiver, "DATARECVR");
dataRecvThread.start();
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("CLOCK");
primaryStage.setMaximized(true);
primaryStage.setScene(new Scene(borderPane, Screen.getPrimary().getVisualBounds().getWidth(), Screen.getPrimary().getVisualBounds().getHeight()));
primaryStage.show();
}
}
DataReceiver.java
import javafx.application.Platform;
import java.time.Instant;
import static java.lang.Thread.sleep;
public class DataReceiver implements Runnable {
public static int deg;
DataReceiver() {
deg = 0;
}
public void run() {
while(true) {
long startTime = Instant.now().toEpochMilli();
deg++;
while(deg >= 360)
deg -= 360;
while(deg < 0)
deg += 360;
long endTime = Instant.now().toEpochMilli();
Platform.runLater(() -> {
MainApp.angHand.endXProperty().bind(MainApp.clockCircle.radiusProperty().multiply(Math.sin(Math.toRadians(deg))));
MainApp.angHand.endYProperty().bind(MainApp.clockCircle.radiusProperty().multiply(-Math.cos(Math.toRadians(deg))));
MainApp.angText.xProperty().bind(MainApp.angHand.endXProperty().add(10 * Math.sin(Math.toRadians(deg))));
MainApp.angText.yProperty().bind(MainApp.angHand.endYProperty().subtract(10 * Math.cos(Math.toRadians(deg))));
MainApp.angText.setText(String.valueOf(deg));
MainApp.angRotate.setAngle(deg);
MainApp.angLabel.setText("Angle: " + deg);
});
try {
sleep(1000 - (endTime - startTime));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我正在使用jdk-13.0.1和javafx-sdk-11.0.2
推荐答案
此处的问题是Group
本身不可调整大小,但会更改其自身大小以适合内容.修改文本的转换会导致Group
的边界水平和/或垂直变化,进而使父级布局重新定位组以使其居中.根据您的需要,您可能希望将时钟包裹在不执行此操作的父母中(Pane
).如果要使圆心居中,则可能需要实现自己的布局.
The problem here is the fact that Group
itself is non-resizeable, but changes it's own size to fit the contents. Modifying the transformations of the text results in the bounds of the Group
changing horizontally and/or vertically which in turn makes the parent layout reposition the group to keep it centered. Depending on your needs you may want wrap the clock in a parent not doing this (Pane
). If you want to keep the center of the circle centered, you'll probably need to implement your own layout.
注释:
- 我不建议使用
Thread
进行此操作.而是使用AnimationTimer
或JavaFX提供的类似实用程序进行此操作. - 绑定仅对于动态更新是必需的.如果只想设置值,那是错误的选择.
- 有余数运算符(
%
)可以处理while
循环之一的逻辑.此外,实际上并不需要第二个循环,因为deg
的值永远不会降低到0以下. - 我不建议在
main
方法中使用线程来放置逻辑.如果在启动工具箱之前调用Platform.runLater
,则实现逻辑的方式可能会导致异常.最好从Application.start
初始化这种逻辑.
如果可能,应避免使用 -
static
,因为这会使控制数据流更加复杂.如果要显示2个不同时区的时钟怎么办?如果类依赖于static
数据,则无法重用该类. -
像下面这样的绑定是没有意义的:
borderPane
是场景的根,因此将保留位置(0,0);此外,angLabel
是borderPane
的后代.我建议不要使用static
BorderPane.alignment
属性告诉标签如何将节点放置在组中.
- I don't recommend doing this using a
Thread
. Instead do this usingAnimationTimer
or similar utilities provided by JavaFX. - Bindings are only necessary for dynamic updates. If you just want to set values, it's the wrong choice.
- There is the remainder operator (
%
) which could deal with the logic of one of thewhile
loops. Furthermore the second loop is not actually needed, since the value ofdeg
is never decreased below 0. - I wouldn't recommend putting the logic using threads in the
main
method. The way you implement the logic could result in an exception, ifPlatform.runLater
is called before the toolkit has been started. Better initialize this kind of logic fromApplication.start
. static
should be avoided, if possible, since it makes controlling the flow of data more complicated. And what if you want to display 2 clocks for different timezones? There's no way of reusing the class, if it relies onstatic
data in the way your classes do.A binding like the following makes no sense:
borderPane
is the root of the scene and therefore will keep the position (0,0); furthermoreangLabel
is a descendant of theborderPane
. I recommend not wrapping the label in a group any use thestatic
BorderPane.alignment
property to tellborderPane
how to position the node.
angLabel.layoutXProperty().bind(borderPane.layoutXProperty());
angLabel.layoutYProperty().bind(borderPane.layoutYProperty());
以下示例使Clock
成为具有LocalTime
类型的属性的Control
并计算子项本身的位置:
The following example makes Clock
a Control
with a property of type LocalTime
and calculates the positions of the children itself:
public class Clock extends Control {
private final ObjectProperty<LocalTime> time = new SimpleObjectProperty<>(this, "time", LocalTime.MIDNIGHT) {
@Override
public void set(LocalTime newValue) {
// make sure the value is non-null
super.set(newValue == null ? LocalTime.MIDNIGHT : newValue);
}
};
public ObjectProperty<LocalTime> timeProperty() {
return time;
}
public LocalTime getTime() {
return time.get();
}
public void setTime(LocalTime value) {
time.set(value);
}
@Override
protected Skin<?> createDefaultSkin() {
return new ClockSkin(this);
}
}
public class ClockSkin extends SkinBase<Clock> {
private static final double SPACE = 20;
private static final double FACTOR = 360d / 60;
private final Circle face;
private final Line secondsHand;
private final Rotate rotate;
private final Text secondsText;
public ClockSkin(Clock control) {
super(control);
face = new Circle(360, Color.RED);
// line straight up from center to circle border
secondsHand = new Line();
secondsHand.endXProperty().bind(face.centerXProperty());
secondsHand.endYProperty().bind(face.centerYProperty().subtract(face.getRadius()));
secondsHand.startXProperty().bind(face.centerXProperty());
secondsHand.startYProperty().bind(face.centerYProperty());
secondsText = new Text();
rotate = new Rotate();
rotate.pivotXProperty().bind(face.centerXProperty());
rotate.pivotYProperty().bind(face.centerYProperty());
secondsHand.getTransforms().add(rotate);
secondsText.getTransforms().add(rotate);
registerChangeListener(control.timeProperty(), (observable) -> {
LocalTime value = (LocalTime) observable.getValue();
update(value);
control.requestLayout();
});
getChildren().addAll(face, secondsHand, secondsText);
update(control.getTime());
}
protected void update(LocalTime time) {
int seconds = time.getSecond();
secondsText.setText(Integer.toString(seconds));
rotate.setAngle(seconds * FACTOR);
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
// center face
face.setCenterX(contentX + SPACE + contentWidth / 2);
face.setCenterY(contentY + SPACE + contentHeight / 2);
// position text
secondsText.setX(contentX + SPACE + (contentWidth - secondsText.prefWidth(-1)) / 2);
secondsText.setY(face.getCenterY() - face.getRadius() - SPACE / 2);
}
@Override
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset,
double leftInset) {
return 2 * (SPACE + face.getRadius()) + leftInset + rightInset;
}
@Override
protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
return 2 * (SPACE + face.getRadius()) + topInset + bottomInset;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset,
double leftInset) {
return computeMinWidth(height, topInset, rightInset, bottomInset, leftInset);
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
return computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
@Override
public void dispose() {
getChildren().clear();
super.dispose();
}
}
@Override
public void start(Stage primaryStage) throws Exception {
BorderPane borderPane = new BorderPane();
Clock clock = new Clock();
clock.setTime(LocalTime.now());
Label angLabel = new Label();
BorderPane.setAlignment(angLabel, Pos.TOP_LEFT);
borderPane.setCenter(clock);
borderPane.setBottom(angLabel);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long now) {
LocalTime time = LocalTime.now();
clock.setTime(time);
angLabel.setText(formatter.format(time));
}
};
timer.start();
primaryStage.setTitle("CLOCK");
primaryStage.setMaximized(true);
primaryStage.setScene(new Scene(borderPane));
primaryStage.show();
}
请注意,在这里,用于更新视觉效果的逻辑和用于更新数据的逻辑之间的分离比在代码中更为清晰.通常,您要防止其他类访问内部逻辑,因为这样可以防止可能会破坏您的控制的外部干扰.
Note that here the separation between the logic for updating the visuals and updating the data is done much more cleanly than in your code. In general you want to prevent access of other classes to internal logic, since this prevents outside interference that could possibly break your control.
这篇关于JavaFx Group的内容位置在转换后会发生变化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!