我正在设计一个计时器应用程序。这是我的 JavaFX 代码:
package com.jammin.timetimer;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;
public class HelloApplication extends Application {
@Override
public void start(Stage PrimaryStage) {
//Adds all the necessary elements into the window
StackPane myStackPane = new StackPane();
VBox myVBox = new VBox(myStackPane);
myVBox.setAlignment(Pos.BOTTOM_CENTER);
HBox myHBox = new HBox(myVBox);
myHBox.setAlignment(Pos.CENTER);
Scene myScene = new Scene(myHBox, 400,400);
ImageView myImageView = new ImageView(new Image("file:src/main/resources/img/timer_ticks.png"));
myImageView.fitHeightProperty().bind(myScene.heightProperty());
myImageView.fitWidthProperty().bind(myScene.widthProperty());
myImageView.setPreserveRatio(true);
myStackPane.getChildren().addAll(myImageView);
Arc myArc = createArc(myStackPane, myScene);
TextField myTextField = createTextField(myArc, myScene);
myVBox.getChildren().add(myTextField);
PrimaryStage.setScene(myScene);
PrimaryStage.show();
System.out.println(myVBox.fillWidthProperty());
}
//Constructor for my Arc
public Arc createArc(StackPane myStackPane, Scene myScene){
Arc myArc = new Arc(100,100,100,100,0,360);
myArc.setFill(Color.GREEN);
myArc.setType(ArcType.ROUND);
myStackPane.getChildren().add(myArc);
myScene.widthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number t1) {
if (t1.doubleValue() <= myScene.getHeight()*1.05){
myArc.setRadiusX(t1.doubleValue()/2.9);
myArc.setRadiusY(t1.doubleValue()/2.9);
}
}
});
myScene.heightProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number t1) {
if (t1.doubleValue() <= myScene.getWidth()){
myArc.setRadiusX(t1.doubleValue()/2.9);
myArc.setRadiusY(t1.doubleValue()/2.9);
}
}
});
return myArc;
}
//Textfield to enter the time
public TextField createTextField(Arc myArc, Scene myScene){
TextField myTextField = new TextField("Enter Time 1-60");
myTextField.setMinHeight(myScene.getHeight()*.1);
TextFormatter<Integer> myTextFormatter = new TextFormatter<>(new IntegerStringConverter(), 60, this::filter);
myTextField.setTextFormatter(myTextFormatter);
// myTextField.setOnAction((evt) -> myArc.setLength(myTextFormatter.getValue()!=null ? 6*myTextFormatter.getValue():0));
return myTextField;
}
//Filter to only allow for use of time between 1-60minutes
public TextFormatter.Change filter(TextFormatter.Change myChange){
if (myChange.getControlNewText().isEmpty()){
return myChange;
}
int myVal = Integer.parseInt(myChange.getControlNewText());
return myVal >=0 && myVal <= 60 ? myChange : null;
}
}
但是,我不喜欢调整大小会导致文本框被截断:
有谁知道解决这个问题的方法吗?我想知道这是否与我用来调整窗口大小的函数有关?但我尝试调整这些值,但没有成功...我不打算在该应用程序的未来版本中保留文本框,但我确实计划放置一个菜单按钮,所以我想尽快解决这个问题稍后。
您有以下行
myImageView.fitHeightProperty().bind(myScene.heightProperty());
它试图强制图像视图的高度为场景的完整高度。这根本没有为文本字段留下任何空间。当没有足够的空间容纳所有子节点时,
VBox
的布局算法将尝试妥协,减少每个子节点的高度。因此,图像视图和文本字段的高度都会减小,因此它们都会被裁剪,尽管这仅对于文本字段很明显(可能图像周围有空白空间)。
通常,使用父节点大小的绑定来控制子节点的大小作为管理布局的方法是一个坏主意。更改子节点的大小将更改其父节点的首选大小,然后绑定将强制重新计算子节点的大小。虽然您可能会侥幸逃脱,但在某些情况下,这可能会强制在每个布局脉冲上重新计算布局。对于一个极端的例子(图像视图的大小不断增大),请参阅此处。
通常对于布局,您应该通过将托管的可调整大小的节点放置在适当的布局窗格(
VBox
,BorderPane
)等中来布局它们,并管理不可调整大小的节点(形状和文本等)的(重要)布局.) 通过使用自定义 Region
,在 layoutChildren()
方法中调整它们的大小和位置。
我不建议在计时器上使用图像作为数字和刻度线:只需使用
Text
和 Line
对象。将它们与圆弧一起放置在自定义 Region
中,并将它们放置在 layoutChildren()
方法中。
这是一个合理的生产就绪实现。我只是硬编码了数字所需的空间并使用了默认字体,但您可以计算文本所需的空间,如果您希望数字随着包含窗格的大小而增加/减少,甚至可以相应地调整字体大小。 (我还按照我认为更自然的顺序排列了数字;如果您希望它们向后排列,就像在OP中一样,您只需更改
angle
中 layoutChildren()
的分配即可。)
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.VPos;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Line;
import javafx.scene.text.Text;
import java.util.ArrayList;
import java.util.List;
public class TimerView extends Region {
private final Arc arc;
private final List<Line> ticks;
private final List<Text> numbers;
private static final double MINOR_TICK_LENGTH = 3;
private static final double MAJOR_TICK_LENGTH = 6;
private static final double NUMBER_PADDING = 30;
private static final double PREF_TIMER_RADIUS = 100;
private static final double MIN_TIMER_RADIUS = 100;
private final DoubleProperty time = new SimpleDoubleProperty(60);
public DoubleProperty timeProperty() {
return time;
}
public final void setTime(double time) {
timeProperty().set(time);
}
public final double getTime() {
return timeProperty().get();
}
public TimerView() {
arc = new Arc();
arc.setStartAngle(90);
arc.setFill(Color.GREEN);
arc.setType(ArcType.ROUND);
arc.lengthProperty().bind(timeProperty().multiply(-6));
ticks = new ArrayList<>();
numbers = new ArrayList<>();
for (int i = 0; i < 60; i++) {
Line tick = new Line();
if (i % 5 == 0) { // major tick:
tick.setStrokeWidth(2);
Text number = new Text(String.valueOf(i));
number.setTextOrigin(VPos.CENTER);
numbers.add(number);
}
ticks.add(tick);
}
getChildren().add(arc);
getChildren().addAll(ticks);
getChildren().addAll(numbers);
}
@Override
protected void layoutChildren() {
double ti = snappedTopInset();
double ri = snappedRightInset();
double bi = snappedBottomInset();
double li = snappedLeftInset();
double availableWidth = getWidth() - li - ri;
double availableHeight = getHeight() - ti - bi;
double centerX = li + availableWidth / 2;
double centerY = ti + availableHeight / 2;
double radius = Math.min(availableWidth / 2, availableHeight / 2);
double arcRadius = radius - MAJOR_TICK_LENGTH - NUMBER_PADDING;
arc.setCenterX(centerX);
arc.setCenterY(centerY);
arc.setRadiusX(arcRadius);
arc.setRadiusY(arcRadius);
for (int i = 0; i < 60; i++) {
double angle = -Math.PI / 2 + 2 * Math.PI * i / 60;
Line tick = ticks.get(i);
double c = Math.cos(angle);
double s = Math.sin(angle);
tick.setStartX(centerX + arcRadius * c);
tick.setStartY(centerY + arcRadius * s);
if (i % 5 == 0) { // major tick
tick.setEndX(centerX + (arcRadius + MAJOR_TICK_LENGTH) * c);
tick.setEndY(centerY + (arcRadius + MAJOR_TICK_LENGTH) * s);
Text number = numbers.get(i / 5);
double numberWidth = number.getBoundsInLocal().getWidth();
number.setX(centerX - numberWidth / 2 + (arcRadius + MAJOR_TICK_LENGTH + NUMBER_PADDING / 2) * c);
number.setY(centerY + (arcRadius + MAJOR_TICK_LENGTH + NUMBER_PADDING / 2) * s);
} else {
tick.setEndX(centerX + (arcRadius + MINOR_TICK_LENGTH) * c);
tick.setEndY(centerY + (arcRadius + MINOR_TICK_LENGTH) * s);
}
}
}
@Override
protected double computePrefWidth(double height) {
return snappedLeftInset() + snappedRightInset() +
2 * (NUMBER_PADDING + MAJOR_TICK_LENGTH + PREF_TIMER_RADIUS);
}
@Override
protected double computePrefHeight(double width) {
return snappedTopInset() + snappedBottomInset() +
2 * (NUMBER_PADDING + MAJOR_TICK_LENGTH + PREF_TIMER_RADIUS);
}
@Override
protected double computeMinWidth(double height) {
return snappedLeftInset() + snappedRightInset() +
2 * (NUMBER_PADDING + MAJOR_TICK_LENGTH + MIN_TIMER_RADIUS);
}
@Override
protected double computeMinHeight(double width) {
return snappedTopInset() + snappedBottomInset() +
2 * (NUMBER_PADDING + MAJOR_TICK_LENGTH + MIN_TIMER_RADIUS);
}
}
这是它在OP中提供的或多或少相同的应用程序示例中使用的:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;
public class HelloApplication extends Application {
@Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
TimerView timerView = new TimerView();
root.setCenter(timerView);
TextField myTextField = createTextField(timerView);
root.setBottom(myTextField);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
//Textfield to enter the time
public TextField createTextField(TimerView timer){
TextField myTextField = new TextField("Enter Time 1-60");
TextFormatter<Integer> myTextFormatter = new TextFormatter<>(new IntegerStringConverter(), 60, this::filter);
myTextField.setTextFormatter(myTextFormatter);
timer.timeProperty().bind(myTextFormatter.valueProperty());
return myTextField;
}
//Filter to only allow for use of time between 1-60minutes
public TextFormatter.Change filter(TextFormatter.Change myChange){
String newText = myChange.getControlNewText();
if (newText.matches("[0-9]*")) {
if (myChange.getControlNewText().isEmpty()) {
return myChange;
}
int myVal = Integer.parseInt(myChange.getControlNewText());
if (myVal >= 0 && myVal <= 60) {
return myChange;
}
}
return null;
}
}
由于计时器位于边框窗格的中心,因此将调整其大小以占用剩余的可用空间(为文本字段分配空间后),并且
layoutChildren()
方法将使圆弧填充可用空间。因此,增加窗口的大小将导致计时器的大小增加。