JavaFx Group的内容位置在转换后会发生变化 [英] JavaFx Group contents position getting changed upon transformation

查看:62
本文介绍了JavaFx Group的内容位置在转换后会发生变化的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在模拟一个罗盘,在该罗盘上,我需要从另一个来源(通过网络)接收角度数据时显示当前角度位置.但是以某种方式,基于角位置将变换应用于文本"节点后,圆组会发生偏移.这是示例代码

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);此外,angLabelborderPane的后代.我建议不要使用static BorderPane.alignment属性告诉标签如何将节点放置在组中.

  • I don't recommend doing this using a Thread. Instead do this using AnimationTimer 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 the while loops. Furthermore the second loop is not actually needed, since the value of deg 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, if Platform.runLater is called before the toolkit has been started. Better initialize this kind of logic from Application.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 on static 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); furthermore angLabel is a descendant of the borderPane. I recommend not wrapping the label in a group any use the static BorderPane.alignment property to tell borderPane 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆