嘿,我需要一些帮助才能让我的标签应用程序正常工作。目前它显示的值如下:
但是,我希望它包裹标签而不是继续水平移动。我将所有 HBox 更改为 FlowPane,但这使它看起来很奇怪并且效果不佳。我也尝试过做 VBox,但它仍然搞砸了。
我希望它看起来像这样:
当前代码:
package application;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.stage.Stage;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class Main3 extends Application{
Color txtColor;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("TagInputFx Demo");
VBox root = new VBox();
AutocompleteMultiSelectionBox tagInput = new AutocompleteMultiSelectionBox();
ObservableSet<String> sug = FXCollections.observableSet();
String[] listOfDBCalls = "Integrated Security=, user=, password=, pwd=, Database=, Encrypt=, Trusted_Connection=, Persist Security Info=, TrustServerCertificate=, User ID=, Initial Catalog=, AttachDbFileName=, Failover Partner=, Asynchronous Processing=, User Instance=, Packet Size=, Column Encryption Setting=, Network Library=, MultipleActiveResultSets=, Data Source=, Server=, Enclave Attestation Url=, Provider=, UID=, Connect Timeout=, Driver=, MARS_Connection=, Proxy Password=, Proxy User Id=, Host=, Pooling=, Max Pool Size=, Connection Lifetime=, Incr Pool Size=, Decr Pool Size=, DBA Privilege=, Load Balancing=, Dbq=, DistribTX=, OledbKey1=, OledbKey2=, ConnectString=, Version=, New=, UseUTF16Encoding=, Legacy Format=, Read Only=, DateTimeFormat=, BinaryGUID=, Cache Size=, Page Size=, Enlist=, Max Page Count=, Journal Mode=, Synchronous=, Compress=, UTF8Encoding=, Timeout=, NoTXN=, SyncPragma=, StepAPI=, LongNames=, Port=, location=, sslmode=, Protocol=, SslMode=, MinPoolSize=, MaxPoolSize=".split(",");
for (String call : listOfDBCalls) {
sug.add(call);
}
tagInput.setSuggestions(sug);
tagInput.setTextColor(Color.BLACK);
Text header = new Text("Tags:");
root.getChildren().addAll(header, tagInput);
Scene scene = new Scene(root, 600, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main3(String[] args) {
launch(args);
}
public class AutocompleteMultiSelectionBox extends HBox {
private final ObservableList<String> tags;
private final ObservableSet<String> suggestions;
private ContextMenu entriesPopup;
private static final int MAX_ENTRIES = 15;
private final TextField inputTextField;
public AutocompleteMultiSelectionBox() {
getStyleClass().setAll("tag-bar");
getStylesheets().add(getClass().getResource("application.css").toExternalForm());
tags = FXCollections.observableArrayList();
suggestions = FXCollections.observableSet();
inputTextField = new TextField();
this.entriesPopup = new ContextMenu();
setListner();
inputTextField.setOnKeyPressed(event -> {
if (event.getCode().equals(KeyCode.BACK_SPACE) && !tags.isEmpty() && inputTextField.getText().isEmpty()) {
String last = tags.get(tags.size() - 1);
String orgTag = last.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(orgTag);
tags.remove(last);
}
} else if (event.getCode().toString() == "ENTER" || event.getCode().toString() == "TAB" && !inputTextField.getText().isEmpty()) {
String newTag = inputTextField.getText();
String orgTag = newTag.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(newTag);
tags.add(newTag);
inputTextField.setText("");
suggestions.remove(orgTag);
suggestions.remove(newTag);
}
} else if (event.getCode().toString() == "ESCAPE") {
inputTextField.clear();
} else if (event.getCode().toString() == "DOWN" || event.getCode().toString() == "UP") {
entriesPopup.getSkin().getNode().lookup(".menu-item").requestFocus();
}
System.out.println(event.getCode().toString());
});
inputTextField.prefHeightProperty().bind(this.heightProperty());
HBox.setHgrow(inputTextField, Priority.ALWAYS);
inputTextField.setBackground(null);
tags.addListener((ListChangeListener.Change<? extends String> change) -> {
while (change.next()) {
if (change.wasPermutated()) {
ArrayList<Node> newSublist = new ArrayList<>(change.getTo() - change.getFrom());
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.add(null);
}
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.set(change.getPermutation(i), getChildren().get(i));
}
getChildren().subList(change.getFrom(), change.getTo()).clear();
getChildren().addAll(change.getFrom(), newSublist);
} else {
if (change.wasRemoved()) {
getChildren().subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
}
if (change.wasAdded()) {
getChildren().addAll(change.getFrom(), change.getAddedSubList().stream().map(Tag::new).collect(
Collectors.toList()));
}
}
}
});
getChildren().add(inputTextField);
}
private TextFlow buildTextFlow(String text, String filter) {
int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase());
Text textBefore = new Text(text.substring(0, filterIndex));
Text textAfter = new Text(text.substring(filterIndex + filter.length()));
Text textFilter = new Text(text.substring(filterIndex, filterIndex + filter.length()));
textFilter.setFill(Color.ORANGE);
textFilter.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));
return new TextFlow(textBefore, textFilter, textAfter);
}
private void setListner() {
inputTextField.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.isEmpty()) {
entriesPopup.hide();
} else {
List<String> filteredEntries = suggestions
.stream()
.filter(e -> e.toLowerCase().contains(newValue.toLowerCase()))
.collect(Collectors.toList());
if (!filteredEntries.isEmpty()) {
populatePopup(filteredEntries, newValue);
if (!entriesPopup.isShowing()) {
entriesPopup.show(this, Side.BOTTOM, 0, 0);
}
} else {
entriesPopup.hide();
}
}
});
focusedProperty().addListener((observableValue, oldValue, newValue) -> entriesPopup.hide());
}
private void populatePopup(List<String> searchResult, String searchRequest) {
List<CustomMenuItem> menuItems = new LinkedList<>();
searchResult.stream()
.limit(MAX_ENTRIES)
.forEach(result -> {
TextFlow textFlow = buildTextFlow(result, searchRequest);
textFlow.prefWidthProperty().bind(AutocompleteMultiSelectionBox.this.widthProperty());
CustomMenuItem item = new CustomMenuItem(textFlow, true);
menuItems.add(item);
item.setOnAction(actionEvent -> {
if (result.endsWith("=")) {
//Dont close it yet.. keep typing
inputTextField.setText(result);
inputTextField.requestFocus();
inputTextField.end();
} else {
//It has values on both sides of the =
//so lets add it to the tag list
tags.add(result);
suggestions.remove(result);
inputTextField.clear();
entriesPopup.hide();
}
});
});
entriesPopup.getItems().clear();
entriesPopup.getItems().addAll(menuItems);
}
public final ObservableList<String> getTags() {
return tags;
}
public final ObservableSet<String> getSuggestions() {
return suggestions;
}
public final void setTextColor(Color c) {
txtColor = c;
}
public final void setSuggestions(ObservableSet<String> suggestions) {
this.suggestions.clear();
this.suggestions.addAll(suggestions);
}
private class Tag extends HBox {
Tag(String tag) {
Button removeButton = new Button();
Image img = new Image(getClass().getResource("delete.png").toExternalForm());
ImageView view = new ImageView(img);
view.setFitHeight(16);
view.setPreserveRatio(true);
getStyleClass().add("tag");
removeButton.setBackground(null);
removeButton.setOnAction(event -> {
tags.remove(tag);
suggestions.add(tag);
inputTextField.requestFocus();
});
Text text = new Text(tag);
text.setFill(txtColor);
text.setFont(Font.font(text.getFont().getFamily(), FontWeight.BOLD, text.getFont().getSize()));
setAlignment(Pos.CENTER);
setSpacing(2);
setPadding(new Insets(0, 0, 0, 5));
removeButton.setGraphic(view);
getChildren().addAll(text, removeButton);
}
}
}
}
有没有办法用 hbox 做到这一点?
更新1
使用 FlowPane 的代码,输出如下所示:
用 FlowPane 替换 HBox 的修改代码:
package application;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.stage.Stage;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class Main3 extends Application{
Color txtColor;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("TagInputFx Demo");
VBox root = new VBox();
AutocompleteMultiSelectionBox tagInput = new AutocompleteMultiSelectionBox();
ObservableSet<String> sug = FXCollections.observableSet();
String[] listOfDBCalls = "Integrated Security=, user=, password=, pwd=, Database=, Encrypt=, Trusted_Connection=, Persist Security Info=, TrustServerCertificate=, User ID=, Initial Catalog=, AttachDbFileName=, Failover Partner=, Asynchronous Processing=, User Instance=, Packet Size=, Column Encryption Setting=, Network Library=, MultipleActiveResultSets=, Data Source=, Server=, Enclave Attestation Url=, Provider=, UID=, Connect Timeout=, Driver=, MARS_Connection=, Proxy Password=, Proxy User Id=, Host=, Pooling=, Max Pool Size=, Connection Lifetime=, Incr Pool Size=, Decr Pool Size=, DBA Privilege=, Load Balancing=, Dbq=, DistribTX=, OledbKey1=, OledbKey2=, ConnectString=, Version=, New=, UseUTF16Encoding=, Legacy Format=, Read Only=, DateTimeFormat=, BinaryGUID=, Cache Size=, Page Size=, Enlist=, Max Page Count=, Journal Mode=, Synchronous=, Compress=, UTF8Encoding=, Timeout=, NoTXN=, SyncPragma=, StepAPI=, LongNames=, Port=, location=, sslmode=, Protocol=, SslMode=, MinPoolSize=, MaxPoolSize=".split(",");
for (String call : listOfDBCalls) {
sug.add(call);
}
tagInput.setSuggestions(sug);
tagInput.setTextColor(Color.BLACK);
Text header = new Text("Tags:");
root.getChildren().addAll(header, tagInput);
Scene scene = new Scene(root, 600, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main3(String[] args) {
launch(args);
}
public class AutocompleteMultiSelectionBox extends FlowPane {
private final ObservableList<String> tags;
private final ObservableSet<String> suggestions;
private ContextMenu entriesPopup;
private static final int MAX_ENTRIES = 15;
private final TextField inputTextField;
public AutocompleteMultiSelectionBox() {
getStyleClass().setAll("tag-bar");
getStylesheets().add(getClass().getResource("application.css").toExternalForm());
tags = FXCollections.observableArrayList();
suggestions = FXCollections.observableSet();
inputTextField = new TextField();
this.entriesPopup = new ContextMenu();
setListner();
inputTextField.setOnKeyPressed(event -> {
if (event.getCode().equals(KeyCode.BACK_SPACE) && !tags.isEmpty() && inputTextField.getText().isEmpty()) {
String last = tags.get(tags.size() - 1);
String orgTag = last.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(orgTag);
tags.remove(last);
}
} else if (event.getCode().toString() == "ENTER" || event.getCode().toString() == "TAB" && !inputTextField.getText().isEmpty()) {
String newTag = inputTextField.getText();
String orgTag = newTag.split("=")[0] + "=";
if (orgTag.length() > 2) {
suggestions.add(newTag);
tags.add(newTag);
inputTextField.setText("");
suggestions.remove(orgTag);
suggestions.remove(newTag);
}
} else if (event.getCode().toString() == "ESCAPE") {
inputTextField.clear();
} else if (event.getCode().toString() == "DOWN" || event.getCode().toString() == "UP") {
entriesPopup.getSkin().getNode().lookup(".menu-item").requestFocus();
}
System.out.println(event.getCode().toString());
//inputTextField.clear();
});
inputTextField.prefHeightProperty().bind(this.heightProperty());
//HBox.setHgrow(inputTextField, Priority.ALWAYS);
inputTextField.setBackground(null);
tags.addListener((ListChangeListener.Change<? extends String> change) -> {
while (change.next()) {
if (change.wasPermutated()) {
ArrayList<Node> newSublist = new ArrayList<>(change.getTo() - change.getFrom());
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.add(null);
}
for (int i = change.getFrom(), end = change.getTo(); i < end; i++) {
newSublist.set(change.getPermutation(i), getChildren().get(i));
}
getChildren().subList(change.getFrom(), change.getTo()).clear();
getChildren().addAll(change.getFrom(), newSublist);
} else {
if (change.wasRemoved()) {
getChildren().subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
}
if (change.wasAdded()) {
getChildren().addAll(change.getFrom(), change.getAddedSubList().stream().map(Tag::new).collect(
Collectors.toList()));
}
}
}
});
getChildren().add(inputTextField);
}
private TextFlow buildTextFlow(String text, String filter) {
int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase());
Text textBefore = new Text(text.substring(0, filterIndex));
Text textAfter = new Text(text.substring(filterIndex + filter.length()));
Text textFilter = new Text(text.substring(filterIndex, filterIndex + filter.length()));
textFilter.setFill(Color.ORANGE);
textFilter.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));
return new TextFlow(textBefore, textFilter, textAfter);
}
private void setListner() {
inputTextField.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.isEmpty()) {
entriesPopup.hide();
} else {
List<String> filteredEntries = suggestions
.stream()
.filter(e -> e.toLowerCase().contains(newValue.toLowerCase()))
.collect(Collectors.toList());
if (!filteredEntries.isEmpty()) {
populatePopup(filteredEntries, newValue);
if (!entriesPopup.isShowing()) {
entriesPopup.show(this, Side.BOTTOM, 0, 0);
}
} else {
entriesPopup.hide();
}
}
});
focusedProperty().addListener((observableValue, oldValue, newValue) -> entriesPopup.hide());
}
private void populatePopup(List<String> searchResult, String searchRequest) {
List<CustomMenuItem> menuItems = new LinkedList<>();
searchResult.stream()
.limit(MAX_ENTRIES)
.forEach(result -> {
TextFlow textFlow = buildTextFlow(result, searchRequest);
textFlow.prefWidthProperty().bind(AutocompleteMultiSelectionBox.this.widthProperty());
CustomMenuItem item = new CustomMenuItem(textFlow, true);
menuItems.add(item);
item.setOnAction(actionEvent -> {
if (result.endsWith("=")) {
//Dont close it yet.. keep typing
inputTextField.setText(result);
inputTextField.requestFocus();
inputTextField.end();
} else {
//It has values on both sides of the =
//so lets add it to the tag list
tags.add(result);
suggestions.remove(result);
inputTextField.clear();
entriesPopup.hide();
}
});
});
entriesPopup.getItems().clear();
entriesPopup.getItems().addAll(menuItems);
}
public final ObservableList<String> getTags() {
return tags;
}
public final ObservableSet<String> getSuggestions() {
return suggestions;
}
public final void setTextColor(Color c) {
txtColor = c;
}
public final void setSuggestions(ObservableSet<String> suggestions) {
this.suggestions.clear();
this.suggestions.addAll(suggestions);
}
private class Tag extends FlowPane {
Tag(String tag) {
Button removeButton = new Button();
Image img = new Image(getClass().getResource("delete.png").toExternalForm());
ImageView view = new ImageView(img);
view.setFitHeight(16);
view.setPreserveRatio(true);
getStyleClass().add("tag");
removeButton.setBackground(null);
removeButton.setOnAction(event -> {
tags.remove(tag);
suggestions.add(tag);
inputTextField.requestFocus();
});
Text text = new Text(tag);
text.setFill(txtColor);
text.setFont(Font.font(text.getFont().getFamily(), FontWeight.BOLD, text.getFont().getSize()));
setAlignment(Pos.CENTER);
//setSpacing(2);
setPadding(new Insets(0, 0, 0, 5));
removeButton.setGraphic(view);
getChildren().addAll(text, removeButton);
}
}
}
}
更新2
这是CSS:
/* JavaFX CSS - Leave this comment until you have at least create one rule which uses -fx-Property */
.tag-bar {
-fx-border-color: #dbdae7;
-fx-spacing: 1;
-fx-padding: 1;
-fx-max-height: 30;
}
.tag-bar .tag {
-fx-border-color: b7b5cf;
-fx-background-color: #dbdae7;
-fx-border-radius: 10;
-fx-background-radius: 10;
}
.tag-bar .tag {
-fx-text-fill: #000;
}
.tag-bar #tagText {
-fx-text-fill: orange;
-fx-font-weight: bold;
}
HBox
不换行:来自 文档:
将其子项布置在单个水平行中HBox
(我的重点)。
要换行,您需要一个
FlowPane
而不是 HBox
。
但是,没有必要用
FlowPane
来表示每个单独的标签:如果这样做,标签的布局将不正确(例如,在这种情况下,流程窗格的首选宽度将默认为 400;再次强调)请参阅文档)。
事实上,正如评论中所指出的,您可以相当轻松地仅用
Label
来表示每个标签,这将避免不必要的子类化。 (通常,您的 UI 代码应遵循所有常见的良好编码实践,例如 “更喜欢聚合而不是继承”。)使用托管节点(例如 Control
和
Region
及其子类)通常会更好地进行布局而不是使用非托管节点,例如
Shape
及其子类(包括
Text
)。这是我认为您正在尝试做的事情的精简版本,其中删除了与布局问题无关的大部分功能以及对外部资源的任何依赖。我只提供了一些标签样式,但这显然可以改进。
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class TagDemo extends Application {
private ObservableList<String> tags;
@Override
public void start(Stage stage) {
tags = FXCollections.observableArrayList();
FlowPane tagPane = new FlowPane();
tagPane.setHgap(5);
tagPane.setVgap(5);
tags.addListener((ListChangeListener.Change<? extends String> change) -> {
while (change.next()) {
if (change.wasRemoved()) {
int from = change.getFrom();
int to = change.getTo();
tagPane.getChildren().subList(from, to + 1).clear();
}
if (change.wasAdded()) {
tagPane.getChildren().addAll(
change.getFrom(),
change.getAddedSubList().stream().map(this::createTag).toList()
);
}
}
});
TextField tagField = new TextField();
tagField.setPromptText("New Tag");
tagField.setOnAction(_ -> {
if (! tagField.getText().isBlank()) {
tags.add(tagField.getText());
tagField.clear();
}
});
tagPane.getChildren().add(tagField);
BorderPane root = new BorderPane(tagPane);
root.setTop(new Label("Tags:"));
Scene scene = new Scene(root, 800, 500);
stage.setScene(scene);
stage.show();
}
private Node createTag(String tagName) {
Label label = new Label(tagName);
Button delete = new Button("X");
delete.setOnAction(_ -> tags.remove(tagName));
label.setGraphic(delete);
label.setContentDisplay(ContentDisplay.RIGHT);
label.setGraphicTextGap(1);
label.setStyle("-fx-color: lightsteelblue; -fx-background-color: lightsteelblue;");
return label;
}
public static void main(String[] args) {
launch();
}
}