【问题标题】:JavaFX: Dynamically update Menu while showingJavaFX:显示时动态更新菜单
【发布时间】:2019-07-16 23:14:18
【问题描述】:

我想在 JavaFX 菜单中添加一个选项列表。显示菜单时应修改选项的顺序(我将使用模糊匹配,但该方法与我的问题无关)。我可以用分别包含TextFieldListViewCustomMenuItems 来模仿这种行为:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.ListView;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuButton;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MWE extends Application {

    @Override
    public void start(Stage primaryStage) {
        final Menu menu = new Menu("MENU");

        final List<String> options = Arrays.asList(
                "AbC",
                "dfjksdljf",
                "skdlfj",
                "stackoverflow");

        final StringProperty currentSelection = new SimpleStringProperty(null);

        final TextField fuzzySearchField = new TextField(null);
        final CustomMenuItem fuzzySearchItem = new CustomMenuItem(fuzzySearchField, false);
        // TODO unfortunately we seem to have to grab focus like this!
        fuzzySearchField.addEventFilter(MouseEvent.MOUSE_MOVED, e->{fuzzySearchField.requestFocus(); fuzzySearchField.selectEnd();});
        final ObservableList<String> currentMatches = FXCollections.observableArrayList();
        // just some dummy matching here
        fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> currentMatches.setAll(options.stream().filter(s -> s.toLowerCase().contains(newv)).collect(Collectors.toList())));
        final ListView<String> lv = new ListView<>(currentMatches);
        lv.addEventFilter(MouseEvent.MOUSE_MOVED, e -> lv.requestFocus());
        final CustomMenuItem lvItem = new CustomMenuItem(lv, false);
        menu.getItems().setAll(fuzzySearchItem, lvItem);
        fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> currentSelection.setValue(currentMatches.size() > 0 ? currentMatches.get(0) : null));
        fuzzySearchField.setText("");

        menu.setOnShown(e -> fuzzySearchField.requestFocus());
        final MenuButton button = new MenuButton("menu");
        button.getItems().setAll(menu);

        Platform.runLater(() -> {
            final Scene scene = new Scene(button);
            primaryStage.setScene(scene);
            primaryStage.show();
        });

    }
}

但是,在菜单结构中包含ListView 感觉很奇怪。这就是为什么我尝试使用MenuItems 而不是ListView

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.ListView;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MWE2 extends Application {

    @Override
    public void start(Stage primaryStage) {
        final Menu menu = new Menu("MENU");

        final List<String> options = Arrays.asList(
                "AbC",
                "dfjksdljf",
                "skdlfj",
                "stackoverflow");

        final StringProperty currentSelection = new SimpleStringProperty(null);

        final TextField fuzzySearchField = new TextField(null);
        final CustomMenuItem fuzzySearchItem = new CustomMenuItem(fuzzySearchField, false);
        // TODO unfortunately we seem to have to grab focus like this!
        fuzzySearchField.addEventFilter(MouseEvent.MOUSE_MOVED, e->{fuzzySearchField.requestFocus(); fuzzySearchField.selectEnd();});
        final ObservableList<String> currentMatches = FXCollections.observableArrayList();
        // just some dummy matching here
        fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> currentMatches.setAll(options.stream().filter(s -> s.toLowerCase().contains(newv)).collect(Collectors.toList())));
        currentMatches.addListener((ListChangeListener<String>)change -> {
            List<MenuItem> items = new ArrayList<>();
            items.add(fuzzySearchItem);
            currentMatches.stream().map(MenuItem::new).forEach(items::add);
            System.out.println("Updating menu items!");
            menu.getItems().setAll(items);
        });
        fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> currentSelection.setValue(currentMatches.size() > 0 ? currentMatches.get(0) : null));
        fuzzySearchField.setText("");

        menu.setOnShown(e -> fuzzySearchField.requestFocus());
        final MenuButton button = new MenuButton("menu");
        button.getItems().setAll(menu);

        Platform.runLater(() -> {
            final Scene scene = new Scene(button);
            primaryStage.setScene(scene);
            primaryStage.show();
        });

    }
}

在那个例子中,菜单在显示时没有更新,即使我可以看到"Updating menu items!" 打印到控制台,所以menu 的项目正在更新。但是,屏幕上的菜单不会改变。

有没有办法请求重绘菜单?

相关问题:

【问题讨论】:

  • 我有一个非常老套的“解决方案”,我不想用作答案:首先,通过反射获取私有方法Menu.setShowing(boolean)。然后,当更新列表并显示菜单时,首先使用false 调用该方法,然后使用true 作为参数调用该方法。同样,非常老套,不是我想要的答案,但它有效(但不知道任何副作用)。
  • 您不需要反射,menu.hidemenu.show 也可以。我可以建议对您的第一个解决方案进行改进 - 您可以设置 ListView 的样式,使其看起来有点像菜单(有点像,不是 100% 像菜单)
  • 谢谢@Enigo 我试过menu.hidemenu.show 但延迟比通过Menu.setShowing 要长得多。其原因对我来说并不完全清楚。我可能会设计ListView。我希望菜单是动态的,即使可见。如果不是这样,我想知道这背后的基本原理是什么。
  • 对我来说看起来像一个错误:如果您直接在 contextMenu 中执行相同操作(不通过子菜单间接),列表将按预期更新。
  • 报告了the bug,初步挖掘了一个肮脏(!)hackaround的原因和轮廓

标签: java listview javafx menu javafx-8


【解决方案1】:

我遵循@Enigo@Jesse_mw 的建议并使用了自定义节点。我决定使用仅包含Labels 的VBox 而不是ListView,因为我只需要基本功能并且不希望有额外的混乱处理程序或突出显示。另请注意,@kleopatra 指出,dynamic updates of items works out of the box for ContextMenu (and a potential bug in Menu),不幸的是,这对于我的用例来说不是一个合适的选择。

这是我的最低工作示例代码:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuButton;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MWE extends Application {

    private static Label label(final String text) {
        final Label label = new Label(text);
        label.addEventFilter(MouseEvent.MOUSE_MOVED, e -> label.requestFocus());
        label.setMaxWidth(200);
        final Background background = label.getBackground();
        label.setOnMouseEntered(e -> label.setBackground(new Background(new BackgroundFill(Color.GRAY.brighter(), CornerRadii.EMPTY, Insets.EMPTY))));
        label.setOnMouseExited(e -> label.setBackground(background));
        // Do something on mouse press; in real world scenario, also hide menu
        label.setOnMousePressed(e -> {
            if (e.isPrimaryButtonDown()) {
                System.out.println(label.getText());
                e.consume();
            }
        });
        return label;
    }

    @Override
    public void start(Stage primaryStage) {
        final Menu menu = new Menu("MENU");

        final List<String> options = Arrays.asList(
                "AbC",
                "dfjksdljf",
                "skdlfj",
                "stackoverflow","ssldkfjsdaf", "sjsdlf", "apple juice", "banana", "mango", "sdlfkjasdlfjsadlfj", "lkjsdflsdfj",
                "stackoverflow","ssldkfjsdaf", "sjsdlf", "apple juice", "banana", "mango", "sdlfkjasdlfjsadlfj", "lkjsdflsdfj",
                "stackoverflowstackoverflowstackoverflowstackoverflowstackoverflowstackoverflow","ssldkfjsdaf", "sjsdlf", "apple juice", "banana", "mango", "sdlfkjasdlfjsadlfj", "lkjsdflsdfj");

        final StringProperty currentSelection = new SimpleStringProperty(null);

        final TextField fuzzySearchField = new TextField(null);
        final CustomMenuItem fuzzySearchItem = new CustomMenuItem(fuzzySearchField, false);
        fuzzySearchItem.setDisable(true);
        // TODO unfortunately we seem to have to grab focus like this!
        fuzzySearchField.addEventFilter(MouseEvent.MOUSE_MOVED, e->{fuzzySearchField.requestFocus(); fuzzySearchField.selectEnd();});
        final ObservableList<String> currentMatches = FXCollections.observableArrayList();
        // just some dummy matching here
        fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> currentMatches.setAll(options.stream().filter(s -> s.toLowerCase().contains(newv)).collect(Collectors.toList())));
        final VBox labels = new VBox();
        currentMatches.addListener((ListChangeListener<String>) change -> labels.getChildren().setAll(currentMatches.stream().map(MWE::label).collect(Collectors.toList())));
        final CustomMenuItem labelItem = new CustomMenuItem(labels, false);
        menu.getItems().setAll(fuzzySearchItem, labelItem);
        fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> currentSelection.setValue(currentMatches.size() > 0 ? currentMatches.get(0) : null));
        fuzzySearchField.setText("");

        menu.setOnShown(e -> fuzzySearchField.requestFocus());
        final MenuButton button = new MenuButton("menu");
        button.getItems().setAll(menu);

        Platform.runLater(() -> {
            final Scene scene = new Scene(button);
            primaryStage.setScene(scene);
            primaryStage.show();
        });

    }
}

【讨论】:

  • 报告了the bug,并初步挖掘了一个肮脏(!)hackaround的原因和轮廓
  • 非常感谢@kleopatra,非常感谢!
【解决方案2】:

要在 JavaFX 中正确地动态更新列表,您可以将 Binding 与可观察列表一起使用,就像我在您的代码中所做的那样。以下代码的唯一问题是,由于 Menu 类的功能,它无法按预期工作,每次更新列表时,菜单都会隐藏。我认为您应该像 cmets 中讨论的那样设置列表视图的样式,然后再次使用以下绑定,因为它适用于我相信的所有使用可观察列表的视图。

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.lang.management.PlatformManagedObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MWE2 extends Application {
public static void main(String[] args) {
    launch(args);
}

@Override
public void start(Stage primaryStage) {
    final Menu menu = new Menu("MENU");
    final MenuButton button = new MenuButton("menu");
    final List<String> options = Arrays.asList(
            "AbC",
            "dfjksdljf",
            "skdlfj",
            "stackoverflow");

    final StringProperty currentSelection = new SimpleStringProperty(null);

    final TextField fuzzySearchField = new TextField(null);
    final CustomMenuItem fuzzySearchItem = new CustomMenuItem(fuzzySearchField, false);
    // TODO unfortunately we seem to have to grab focus like this!
    fuzzySearchField.addEventFilter(MouseEvent.MOUSE_MOVED, e -> {
        fuzzySearchField.requestFocus();
        fuzzySearchField.selectEnd();
    });
    final ObservableList<String> currentMatches = FXCollections.observableArrayList();
    // just some dummy matching here
    fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> {

            currentMatches.setAll(options.stream().filter(s -> s.toLowerCase().contains(newv)).collect(Collectors.toList()));

    });
   //changed from ArrayList to ObservableArray
    ObservableList<MenuItem> items =  FXCollections.observableArrayList(); 
    currentMatches.addListener((ListChangeListener<String>) change -> {
        items.clear();//Clearing items to in-case of duplicates and NULL duplicates.
        items.add(fuzzySearchItem);

        currentMatches.stream().map(MenuItem::new).forEach(items::add);

        System.out.println("Updating menu items!");
        menu.getItems().setAll(items);


    });
    // Binding to Observable items.
    Bindings.bindContent(menu.getItems(), items); 
    fuzzySearchField.textProperty().addListener((obs, oldv, newv) -> currentSelection.setValue(currentMatches.size() > 0 ? currentMatches.get(0) : null));
    fuzzySearchField.setText("");


    button.getItems().setAll(menu);


    final Scene scene = new Scene(button);

    primaryStage.setScene(scene);
    primaryStage.show();

}

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-01-23
    • 1970-01-01
    • 2011-09-12
    • 2023-03-06
    • 2012-01-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多