如何让JavaFX TextFlow自动裁剪一段很长的文本?
How to let JavaFX TextFlow automatically cut a long text?
我想使用 TextFlow 显示一些格式化的文本。以前,我使用了一个简单的标签(wrapText 设置为 true)来显示该文本(未格式化),但我想使用一个库来提供我想使用 TextFlow 显示的文本列表。
我的问题是我要显示的文本比可用区域大。当 运行 超出 space 时,标签会截断文本。这很好用。不幸的是,TextFlow 没有。当文本变得太长时,它会溢出 TextFlow 所在的区域。然后相邻的 TextFlow 会相互重叠。我怎样才能模仿标签的行为?
可以在 here 及以下找到 MWE。
我使用带有两列的 GridPane。左侧三个 TextFlow,右侧三个 Label。显示的文本对于所有六个元素都是相同的。
它产生这个 window:
如您所见,左侧(在 TextFlows 中)的文本重叠。
我试过了,没有成功:
- 将 TextFlow 的 maxWidth 和 maxHeight 设置为可用区域
- 创建一个合适大小的矩形并将其设置为剪辑
JAVA:
package sample;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class Main extends Application {
@FXML
private TextFlow textFlow0;
@FXML
private TextFlow textFlow1;
@FXML
private TextFlow textFlow2;
@FXML
private Label label0;
@FXML
private Label label1;
@FXML
private Label label2;
private String longText = "This is some really long text that should overflow the available Area. " +
"For TextFields, this is handeled by cropping the text to appropriate length and adding \"...\" at the end. " +
"No such option exists for TextFlows";
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Text Overflow");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
@FXML
private void initialize() {
textFlow0.getChildren().add(new Text(longText));
textFlow1.getChildren().add(new Text(longText));
textFlow2.getChildren().add(new Text(longText));
label0.setText(longText);
label1.setText(longText);
label2.setText(longText);
}
public static void main(String[] args) {
launch(args);
}
}
FXML:
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Main"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<TextFlow fx:id="textFlow0" GridPane.rowIndex = "0" GridPane.columnIndex="0" />
<Label fx:id="label0" GridPane.rowIndex = "0" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow1" GridPane.rowIndex = "1" GridPane.columnIndex="0" />
<Label fx:id="label1" GridPane.rowIndex = "1" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow2" GridPane.rowIndex = "2" GridPane.columnIndex="0" />
<Label fx:id="label2" GridPane.rowIndex = "2" wrapText="true" GridPane.columnIndex="1"/>
</GridPane>
失败:尝试使用剪辑
package sample;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class Main extends Application {
@FXML
private FlowPane flowPane;
@FXML
private TextFlow textFlow0;
@FXML
private TextFlow textFlow1;
@FXML
private TextFlow textFlow2;
@FXML
private Label label0;
@FXML
private Label label1;
@FXML
private Label label2;
private String longText = "This is some really long text that should overflow the available Area. " +
"For TextFields, this is handeled by cropping the text to appropriate length and adding \"...\" at the end. " +
"No such option exists for TextFlows";
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Text Overflow");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
@FXML
private void initialize() {
flowPane.setPrefWrapLength(Double.MAX_VALUE);
Rectangle rect = new Rectangle();
rect.widthProperty().bind(flowPane.widthProperty());
rect.heightProperty().bind(flowPane.heightProperty());
flowPane.setClip(rect);
textFlow0.getChildren().add(new Text(longText));
textFlow1.getChildren().add(new Text(longText));
textFlow2.getChildren().add(new Text(longText));
label0.setText(longText);
label1.setText(longText);
label2.setText(longText);
}
public static void main(String[] args) {
launch(args);
}
}
剪辑尝试的 FXML 文件
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.FlowPane?>
<GridPane fx:controller="sample.Main"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<FlowPane fx:id="flowPane" GridPane.rowIndex = "0" GridPane.columnIndex="0">
<TextFlow fx:id="textFlow0" />
</FlowPane>
<Label fx:id="label0" GridPane.rowIndex = "0" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow1" GridPane.rowIndex = "1" GridPane.columnIndex="0" />
<Label fx:id="label1" GridPane.rowIndex = "1" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow2" GridPane.rowIndex = "2" GridPane.columnIndex="0" />
<Label fx:id="label2" GridPane.rowIndex = "2" wrapText="true" GridPane.columnIndex="1"/>
</GridPane>
由于似乎没有内置的方法来执行此操作,因此我实现了自己的方法。
它可能不是解决此问题的最有效方法,但可以很好地满足我的用例。
如果接下来几天出现更好的解决方案,我会接受其中之一。如果没有,我会 select 这个答案被接受。
还有一个问题:我需要单击 window 一次,文本才会显示在开头。
还有一个大问题:子节点不是Text对象怎么办?
package sample;
import javafx.beans.DefaultProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
@DefaultProperty("children")
public class EllipsingTextFlow extends TextFlow {
private final static String DEFAULT_ELLIPSIS_STRING = "...";
private StringProperty ellipsisString;
//private ListProperty<Node> allChildren = new SimpleListProperty<Node>(new SimpleObs<Node>());
private ObservableList<Node> allChildren = FXCollections.observableArrayList();
private ChangeListener sizeChangeListener = (observableValue, number, t1) -> adjustText();
public EllipsingTextFlow() {
allChildren.addListener((ListChangeListener<Node>) this::adjustChildren);
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
adjustText();
}
@Override
public ObservableList<Node> getChildren() {
return allChildren;
}
private void adjustChildren(ListChangeListener.Change<? extends Node> change) {
while (change.next()) {
if (change.wasRemoved()) {
super.getChildren().remove(change.getFrom(), change.getTo());
} else if (change.wasAdded()) {
super.getChildren().addAll(change.getFrom(), change.getAddedSubList());
}
}
adjustText();
}
private void adjustText() {
// remove listeners
widthProperty().removeListener(sizeChangeListener);
heightProperty().removeListener(sizeChangeListener);
while (getHeight() > getMaxHeight() || getWidth() > getMaxWidth()) {
if (super.getChildren().isEmpty()) {
// nothing fits
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
super.getChildren().remove(super.getChildren().size()-1);
super.autosize();
}
while (getHeight() <= getMaxHeight() && getWidth() <= getMaxWidth()) {
if (super.getChildren().size() == allChildren.size()) {
if (allChildren.size() > 0) {
// all Texts are displayed, let's make sure all chars are as well
Node lastChildAsShown = super.getChildren().get(super.getChildren().size() - 1);
Node lastChild = allChildren.get(allChildren.size() - 1);
if (lastChildAsShown instanceof Text && ((Text) lastChildAsShown).getText().length() < ((Text) lastChild).getText().length()) {
((Text) lastChildAsShown).setText(((Text) lastChild).getText());
} else {
// nothing to fill the space with
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
}
} else {
super.getChildren().add(allChildren.get(super.getChildren().size()));
}
super.autosize();
}
// ellipse the last text as much as necessary
while (getHeight() > getMaxHeight() || getWidth() > getMaxWidth()) {
Node lastChildAsShown = super.getChildren().remove(super.getChildren().size()-1);
while (getEllipsisString().equals(((Text) lastChildAsShown).getText())) {
if (super.getChildren().size() == 0) {
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
lastChildAsShown = super.getChildren().remove(super.getChildren().size() -1);
}
if (lastChildAsShown instanceof Text && ((Text) lastChildAsShown).getText().length() > 0) {
// Text shortenedChild = new Text(((Text) lastChildAsShown).getText().substring(0, ((Text) lastChildAsShown).getText().length()-1));
Text shortenedChild = new Text(ellipseString(((Text) lastChildAsShown).getText()));
super.getChildren().add(shortenedChild);
} else {
// don't know what to do with anything else. Leave without adding listeners
return;
}
super.autosize();
}
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
}
private String ellipseString(String s) {
int spacePos = s.lastIndexOf(' ');
if (spacePos < 0) {
return getEllipsisString();
}
return s.substring(0, spacePos) + getEllipsisString();
}
public final void setEllipsisString(String value) {
ellipsisString.set((value == null) ? "" : value);
}
public String getEllipsisString() {
return ellipsisString == null ? DEFAULT_ELLIPSIS_STRING : ellipsisString.get();
}
public final StringProperty ellipsisStringProperty(){
return ellipsisString;
}
}
我想使用 TextFlow 显示一些格式化的文本。以前,我使用了一个简单的标签(wrapText 设置为 true)来显示该文本(未格式化),但我想使用一个库来提供我想使用 TextFlow 显示的文本列表。
我的问题是我要显示的文本比可用区域大。当 运行 超出 space 时,标签会截断文本。这很好用。不幸的是,TextFlow 没有。当文本变得太长时,它会溢出 TextFlow 所在的区域。然后相邻的 TextFlow 会相互重叠。我怎样才能模仿标签的行为?
可以在 here 及以下找到 MWE。 我使用带有两列的 GridPane。左侧三个 TextFlow,右侧三个 Label。显示的文本对于所有六个元素都是相同的。 它产生这个 window:
如您所见,左侧(在 TextFlows 中)的文本重叠。
我试过了,没有成功:
- 将 TextFlow 的 maxWidth 和 maxHeight 设置为可用区域
- 创建一个合适大小的矩形并将其设置为剪辑
JAVA:
package sample;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class Main extends Application {
@FXML
private TextFlow textFlow0;
@FXML
private TextFlow textFlow1;
@FXML
private TextFlow textFlow2;
@FXML
private Label label0;
@FXML
private Label label1;
@FXML
private Label label2;
private String longText = "This is some really long text that should overflow the available Area. " +
"For TextFields, this is handeled by cropping the text to appropriate length and adding \"...\" at the end. " +
"No such option exists for TextFlows";
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Text Overflow");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
@FXML
private void initialize() {
textFlow0.getChildren().add(new Text(longText));
textFlow1.getChildren().add(new Text(longText));
textFlow2.getChildren().add(new Text(longText));
label0.setText(longText);
label1.setText(longText);
label2.setText(longText);
}
public static void main(String[] args) {
launch(args);
}
}
FXML:
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Main"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<TextFlow fx:id="textFlow0" GridPane.rowIndex = "0" GridPane.columnIndex="0" />
<Label fx:id="label0" GridPane.rowIndex = "0" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow1" GridPane.rowIndex = "1" GridPane.columnIndex="0" />
<Label fx:id="label1" GridPane.rowIndex = "1" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow2" GridPane.rowIndex = "2" GridPane.columnIndex="0" />
<Label fx:id="label2" GridPane.rowIndex = "2" wrapText="true" GridPane.columnIndex="1"/>
</GridPane>
失败:尝试使用剪辑
package sample;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
public class Main extends Application {
@FXML
private FlowPane flowPane;
@FXML
private TextFlow textFlow0;
@FXML
private TextFlow textFlow1;
@FXML
private TextFlow textFlow2;
@FXML
private Label label0;
@FXML
private Label label1;
@FXML
private Label label2;
private String longText = "This is some really long text that should overflow the available Area. " +
"For TextFields, this is handeled by cropping the text to appropriate length and adding \"...\" at the end. " +
"No such option exists for TextFlows";
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Text Overflow");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
@FXML
private void initialize() {
flowPane.setPrefWrapLength(Double.MAX_VALUE);
Rectangle rect = new Rectangle();
rect.widthProperty().bind(flowPane.widthProperty());
rect.heightProperty().bind(flowPane.heightProperty());
flowPane.setClip(rect);
textFlow0.getChildren().add(new Text(longText));
textFlow1.getChildren().add(new Text(longText));
textFlow2.getChildren().add(new Text(longText));
label0.setText(longText);
label1.setText(longText);
label2.setText(longText);
}
public static void main(String[] args) {
launch(args);
}
}
剪辑尝试的 FXML 文件
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.FlowPane?>
<GridPane fx:controller="sample.Main"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<FlowPane fx:id="flowPane" GridPane.rowIndex = "0" GridPane.columnIndex="0">
<TextFlow fx:id="textFlow0" />
</FlowPane>
<Label fx:id="label0" GridPane.rowIndex = "0" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow1" GridPane.rowIndex = "1" GridPane.columnIndex="0" />
<Label fx:id="label1" GridPane.rowIndex = "1" wrapText="true" GridPane.columnIndex="1"/>
<TextFlow fx:id="textFlow2" GridPane.rowIndex = "2" GridPane.columnIndex="0" />
<Label fx:id="label2" GridPane.rowIndex = "2" wrapText="true" GridPane.columnIndex="1"/>
</GridPane>
由于似乎没有内置的方法来执行此操作,因此我实现了自己的方法。 它可能不是解决此问题的最有效方法,但可以很好地满足我的用例。 如果接下来几天出现更好的解决方案,我会接受其中之一。如果没有,我会 select 这个答案被接受。
还有一个问题:我需要单击 window 一次,文本才会显示在开头。 还有一个大问题:子节点不是Text对象怎么办?
package sample;
import javafx.beans.DefaultProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
@DefaultProperty("children")
public class EllipsingTextFlow extends TextFlow {
private final static String DEFAULT_ELLIPSIS_STRING = "...";
private StringProperty ellipsisString;
//private ListProperty<Node> allChildren = new SimpleListProperty<Node>(new SimpleObs<Node>());
private ObservableList<Node> allChildren = FXCollections.observableArrayList();
private ChangeListener sizeChangeListener = (observableValue, number, t1) -> adjustText();
public EllipsingTextFlow() {
allChildren.addListener((ListChangeListener<Node>) this::adjustChildren);
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
adjustText();
}
@Override
public ObservableList<Node> getChildren() {
return allChildren;
}
private void adjustChildren(ListChangeListener.Change<? extends Node> change) {
while (change.next()) {
if (change.wasRemoved()) {
super.getChildren().remove(change.getFrom(), change.getTo());
} else if (change.wasAdded()) {
super.getChildren().addAll(change.getFrom(), change.getAddedSubList());
}
}
adjustText();
}
private void adjustText() {
// remove listeners
widthProperty().removeListener(sizeChangeListener);
heightProperty().removeListener(sizeChangeListener);
while (getHeight() > getMaxHeight() || getWidth() > getMaxWidth()) {
if (super.getChildren().isEmpty()) {
// nothing fits
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
super.getChildren().remove(super.getChildren().size()-1);
super.autosize();
}
while (getHeight() <= getMaxHeight() && getWidth() <= getMaxWidth()) {
if (super.getChildren().size() == allChildren.size()) {
if (allChildren.size() > 0) {
// all Texts are displayed, let's make sure all chars are as well
Node lastChildAsShown = super.getChildren().get(super.getChildren().size() - 1);
Node lastChild = allChildren.get(allChildren.size() - 1);
if (lastChildAsShown instanceof Text && ((Text) lastChildAsShown).getText().length() < ((Text) lastChild).getText().length()) {
((Text) lastChildAsShown).setText(((Text) lastChild).getText());
} else {
// nothing to fill the space with
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
}
} else {
super.getChildren().add(allChildren.get(super.getChildren().size()));
}
super.autosize();
}
// ellipse the last text as much as necessary
while (getHeight() > getMaxHeight() || getWidth() > getMaxWidth()) {
Node lastChildAsShown = super.getChildren().remove(super.getChildren().size()-1);
while (getEllipsisString().equals(((Text) lastChildAsShown).getText())) {
if (super.getChildren().size() == 0) {
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
return;
}
lastChildAsShown = super.getChildren().remove(super.getChildren().size() -1);
}
if (lastChildAsShown instanceof Text && ((Text) lastChildAsShown).getText().length() > 0) {
// Text shortenedChild = new Text(((Text) lastChildAsShown).getText().substring(0, ((Text) lastChildAsShown).getText().length()-1));
Text shortenedChild = new Text(ellipseString(((Text) lastChildAsShown).getText()));
super.getChildren().add(shortenedChild);
} else {
// don't know what to do with anything else. Leave without adding listeners
return;
}
super.autosize();
}
widthProperty().addListener(sizeChangeListener);
heightProperty().addListener(sizeChangeListener);
}
private String ellipseString(String s) {
int spacePos = s.lastIndexOf(' ');
if (spacePos < 0) {
return getEllipsisString();
}
return s.substring(0, spacePos) + getEllipsisString();
}
public final void setEllipsisString(String value) {
ellipsisString.set((value == null) ? "" : value);
}
public String getEllipsisString() {
return ellipsisString == null ? DEFAULT_ELLIPSIS_STRING : ellipsisString.get();
}
public final StringProperty ellipsisStringProperty(){
return ellipsisString;
}
}