JavaFX 17 -> 自定义文本区域/文本字段右键菜单

问题描述 投票:0回答:3

我想问一个小问题。 事实上,我想自定义在文本区域或文本字段中右键单击时出现的菜单。 我的目标是通过添加我想要的按钮来保留基本菜单(复制、粘贴、剪切...)。

我发现这篇文章解释了如何做到这一点: JavaFX 附加到 TextField 的右键菜单

import com.sun.javafx.scene.control.skin.TextFieldSkin;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class GiveMeContext extends Application {
    @Override
    public void start(final Stage stage) throws Exception {
        TextField textField = new TextField();
        TextFieldSkin customContextSkin = new TextFieldSkin(textField) {
            @Override
            public void populateContextMenu(ContextMenu contextMenu) {
                super.populateContextMenu(contextMenu);
                contextMenu.getItems().add(0, new SeparatorMenuItem());
                contextMenu.getItems().add(0, new MenuItem("Register"));
            }
        };
        textField.setSkin(customContextSkin);

        stage.setScene(new Scene(textField));
        stage.show();
    }
    public static void main(String[] args) throws Exception {
        launch(args);
    }
}

经过尝试,它在java 8上运行得非常好,但是正如他们当时所说的那样,在java 9之后,它不再运行了。

我尝试替换有问题的方法(populateContextMenu),但不幸的是我找不到任何方法。

如果有人向我展示如何使用 java 9+ 来做到这一点,我将非常感激

java textarea contextmenu textfield javafx-17
3个回答
1
投票

由于模块化,您的代码将无法在 JavaFX 9+ 中运行。有关详细信息,请阅读this。您唯一可以做的就是使用上下文菜单并用您自己的值填充它。下面是在 JavaFX 17 中执行此操作的完整示例。

第1步.创建新项目。

Pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mycompany</groupId>
    <artifactId>mavenproject1</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.9.0</version>
            </plugin>
        </plugins>
    </build>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
    
    <dependencies>
           <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-base</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-fxml</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-graphics</artifactId>
                <version>17.0.2-ea+2</version>
                <scope>compile</scope>
            </dependency>
    </dependencies>
</project>

模块信息:

module Mavenproject1 {
    requires javafx.controls;
    requires javafx.base;
    requires javafx.fxml;
    requires javafx.graphics;
    opens com.mycompany;
}

主要课程:

package com.mycompany;


import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class NewMain2 extends Application {
    
    @Override
    public void start(final Stage stage) throws Exception {
        TextField textField = new TextField();
        
        ContextMenu contextMenu = new ContextMenu();
        MenuItem menuItem1 = new MenuItem("Choice 1");
        MenuItem menuItem2 = new MenuItem("Choice 2");
        MenuItem menuItem3 = new MenuItem("Choice 3");
        contextMenu.getItems().addAll(menuItem1, menuItem2, menuItem3);
        
        textField.setContextMenu(contextMenu);

        stage.setScene(new Scene(textField));
        stage.show();
    }
    public static void main(String[] args) throws Exception {
        launch(args);
    }
}

第 2 步:构建您的项目。

步骤 3. 从此处下载 JavaFX SDK。

第 4 步以这种方式运行您的项目

 java --module-path ./mavenproject1-1.0-SNAPSHOT.jar:/opt/javafx-sdk-17.0.2/lib --add-modules=javafx.controls,javafx.fxml -m Mavenproject1/com.mycompany.NewMain2

1
投票

经过长时间的编程,我找到了一种“扩展”TextInputControl 的默认上下文菜单的方法。我必须从头开始重建它,但它并不像看起来那么复杂。

我的代码:

import java.util.Collection;
import java.util.ResourceBundle;
import java.util.function.Consumer;

import org.apache.commons.lang3.StringUtils;

import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextInputControl;

public interface JFXTextUtils {

    static void initializeContextMenu(TextInputControl textField) {
        final MenuItem undoMI = new ContextMenuItem("Undo", textField, TextInputControl::undo);
        final MenuItem redoMI = new ContextMenuItem("Redo", textField, TextInputControl::redo);
        final MenuItem cutMI = new ContextMenuItem("Cut", textField, TextInputControl::cut);
        final MenuItem copyMI = new ContextMenuItem("Copy", textField, TextInputControl::copy);
        final MenuItem pasteMI = new ContextMenuItem("Paste", textField, TextInputControl::paste);
        final MenuItem selectAllMI = new ContextMenuItem("SelectAll", textField, TextInputControl::selectAll);
        final MenuItem deleteMI = new ContextMenuItem("DeleteSelection", textField, JFXTextUtils::deleteSelectedText);

        textField.undoableProperty().addListener((obs, oldValue, newValue) -> undoMI.setDisable(!newValue));
        textField.redoableProperty().addListener((obs, oldValue, newValue) -> redoMI.setDisable(!newValue));
        textField.selectionProperty().addListener((obs, oldValue, newValue) -> {
            cutMI.setDisable(newValue.getLength() == 0);
            copyMI.setDisable(newValue.getLength() == 0);
            deleteMI.setDisable(newValue.getLength() == 0);
            selectAllMI.setDisable(newValue.getLength() == newValue.getEnd());
        });

        undoMI.setDisable(true);
        redoMI.setDisable(true);
        cutMI.setDisable(true);
        copyMI.setDisable(true);
        deleteMI.setDisable(true);
        selectAllMI.setDisable(true);

        textField.setContextMenu(ContextMenu(undoMI, redoMI, cutMI, copyMI, pasteMI, deleteMI, new SeparatorMenuItem(), selectAllMI,
                new SeparatorMenuItem()));
    }

    static void deleteSelectedText(TextInputControl t) {
        IndexRange range = t.getSelection();
        if (range.getLength() == 0) {
            return;
        }
        String text = t.getText();
        String newText = text.substring(0, range.getStart()) + text.substring(range.getEnd());
        t.setText(newText);
        t.positionCaret(range.getStart());
    }

    class ContextMenuItem extends MenuItem {
        ContextMenuItem(final String action, TextInputControl textField, Consumer<TextInputControl> function) {
            super(ResourceBundle.getBundle("com/sun/javafx/scene/control/skin/resources/controls")
                    .getString("TextInputControl.menu." + action));
            setOnAction(e -> function.accept(textField));
        }
    }

}

此代码准确地重新创建了默认上下文菜单,并准备好在最后一个 MenuSeparator 之后接受更多 MenuItem。


0
投票

正如 @user5182503 (Pavel K. ?) 观察到的,在 JavaFX 9+ 中,不允许访问包含所需属性包的包。

但是,有一个新的 URL 方案

jrt:
可以从运行时读取内容。

这是使用该新功能的答案。

它是在 Windows 11 Pro 下使用 Azul Systems Inc. 的 Zulu JDK FX 17 运行时编写和测试的,并且基于 @Silvio Barbieri 提交的答案。

希望你喜欢:

package com.stackoverflow.q71053358;

import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.StringJoiner;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

/**
 * Example for
 * <a href="https://stackoverflow.com/questions/71053358/">Stackoverflow Question 71053358</a>
 * <br><br>
 * Tested with Zulu JavaFX JDK 17.
 * <br><br>
 * Demonstrates use of the <code>jrt:</code> URL Scheme to access
 * Properties in Packages that in recent JDK's are not accessible.
 */
public class EmulateDefaultContextMenu extends Application {

    private static final class JrtURL {

        private static final String JAVA_RUNTIME_SCHEME = "jrt:";

        private        final URL    url;

        public JrtURL(final String module, final String package_, final String member) throws MalformedURLException {
            this.url = new URL(new StringJoiner("/")
                    .add(JAVA_RUNTIME_SCHEME)
                    .add(module)
                    .add(package_)
                    .add(member)
                    .toString());
        }

        public InputStream openStream() throws IOException {
            return this.url.openStream();
        }
    }

    private static final class Key {

        public final String key;

        public Key(final String... keyParts) {
            this.key = Stream.of(keyParts).collect(Collectors.joining());
        }
        public String lookupString(final ResourceBundle bundle) {
            return bundle.getString(this.key);
        }
    }

    public  static enum Ability {
        ENABLED,
        DISABLED;

        public boolean isEnabled()  {return this == ENABLED;}
        public boolean isDisabled() {return this == DISABLED;}
    }

    private static enum LogSeverity {
        ERROR,  // <- High Severity
        WARN,
        INFO,
        DEBUG,
        TRACE;  // <- Low Severity
    }

    private static final String   TEXT_AREA_MODULE     = "javafx.controls";
    private static final String   TEXT_AREA_PKG        = "com/sun/javafx/scene/control/skin/resources";
    private static final String   TEXT_AREA_PROPS      = "controls.properties";
    private static final String   TEXT_AREA_PROPS_DE   = "controls_de.properties";

    private static final String   TEXT_AREA_MENU       = "TextInputControl.menu.";

    private static final Key      TEXT_AREA_UNDO       = new Key(TEXT_AREA_MENU, "Undo");
    private static final Key      TEXT_AREA_REDO       = new Key(TEXT_AREA_MENU, "Redo");
    private static final Key      TEXT_AREA_CUT        = new Key(TEXT_AREA_MENU, "Cut");
    private static final Key      TEXT_AREA_COPY       = new Key(TEXT_AREA_MENU, "Copy");
    private static final Key      TEXT_AREA_PASTE      = new Key(TEXT_AREA_MENU, "Paste");
    private static final Key      TEXT_AREA_DELETE     = new Key(TEXT_AREA_MENU, "DeleteSelection");
    private static final Key      TEXT_AREA_SELECT_ALL = new Key(TEXT_AREA_MENU, "SelectAll");

    private        final TextArea logTextArea          = new TextArea();

    @Override
    public void start(final Stage primaryStage) throws Exception {
        /*
         * Set up Logging ScrollPane...
         */
        final var logScrollPane = new ScrollPane(logTextArea);

        logTextArea.setStyle   ("-fx-font-family: 'monospaced'");
        logTextArea.setEditable(false); // Side-effect.: CTRL-A, CTRL-C & CTRL-X are ignored

        logTextArea.addEventFilter(KeyEvent.KEY_PRESSED, e -> {

            if (e.isShortcutDown()) { // (CTRL on Win, META on Mac)

                if (e.getCode() == KeyCode.Y     // Suppress CTRL-Y
                ||  e.getCode() == KeyCode.Z) {  // Suppress CTRL-Z
                    e.consume();
                }
            }
        });

        logScrollPane.setHbarPolicy (AS_NEEDED);
        logScrollPane.setVbarPolicy (AS_NEEDED);
        logScrollPane.setFitToHeight(true);
        logScrollPane.setFitToWidth (true);

        /*
         * Generate the Context Menu...
         */
        try {
            final var jrtURL        = new JrtURL(TEXT_AREA_MODULE, TEXT_AREA_PKG, TEXT_AREA_PROPS);
            final var jrtURL_de     = new JrtURL(TEXT_AREA_MODULE, TEXT_AREA_PKG, TEXT_AREA_PROPS_DE);

            final var nullBundle    = getNullBundle();                          // Failing-all-else.: use Key as Title
            final var bundle_en     = getPropertyBundle(jrtURL,    nullBundle); // Fallback to English Titles
            final var bundle        = getPropertyBundle(jrtURL_de, bundle_en);  // German Titles, if available

            final var contextMenu   = newContextMenu(logTextArea);
            /*
             * For completeness, the following Items are ALL those that would be generated for a fully-enabled TextArea.
             * As our TextArea is not editable and CTRL-Y & CTRL-Z are ignored, some are superfluous.
             * The superfluous are assigned to a null Context Menu (i.e. none) & will therefore not appear.
             * Nevertheless, the Listeners for the full functionality are included.
             */
            final var itemUndo      = addMenuItem (null,        bundle, TEXT_AREA_UNDO,       Ability.DISABLED, e -> logTextArea.undo());
            final var itemRedo      = addMenuItem (null,        bundle, TEXT_AREA_REDO,       Ability.DISABLED, e -> logTextArea.redo());
            final var itemCut       = addMenuItem (null,        bundle, TEXT_AREA_CUT,        Ability.DISABLED, e -> logTextArea.cut());
            final var itemCopy      = addMenuItem (contextMenu, bundle, TEXT_AREA_COPY,       Ability.DISABLED, e -> logTextArea.copy());
            ;                         addMenuItem (null,        bundle, TEXT_AREA_PASTE,      Ability.ENABLED,  e -> logTextArea.paste());
            final var itemDelete    = addMenuItem (null,        bundle, TEXT_AREA_DELETE,     Ability.DISABLED, e -> deleteSelectedText());
            ;                         addSeparator(null);
            final var itemSelectAll = addMenuItem (contextMenu, bundle, TEXT_AREA_SELECT_ALL, Ability.DISABLED, e -> logTextArea.selectAll());
            ;                         addSeparator(contextMenu);
            ;                         addSeparator(contextMenu);
            ;                         addMenuItem (contextMenu,         "Change Log Level",   Ability.ENABLED,  e -> changeLogThreshold());

            logTextArea.undoableProperty() .addListener((obs, oldValue, newValue) -> itemUndo.setDisable(!newValue));
            logTextArea.redoableProperty() .addListener((obs, oldValue, newValue) -> itemRedo.setDisable(!newValue));
            logTextArea.selectionProperty().addListener((obs, oldValue, newValue) -> {
                itemCut      .setDisable(newValue.getLength() == 0);
                itemCopy     .setDisable(newValue.getLength() == 0);
                itemDelete   .setDisable(newValue.getLength() == 0);
                itemSelectAll.setDisable(newValue.getLength() == newValue.getEnd());
            });
        } catch (final IOException e) {
            e.printStackTrace();
        }

        /*
         * Set the Scene...
         */
        primaryStage.setTitle("Question 71053358");
        primaryStage.setScene(new Scene(logScrollPane, 480, 320));
        primaryStage.show();

        /*
         * Generate some Content every now-and-again...
         */
        final Runnable runnable  = () -> {
            Platform.runLater(() -> logTextArea.appendText(ZonedDateTime.now().toString() + '\n'));
        };
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(runnable, 2, 9, TimeUnit.SECONDS);
    }

    private static final PropertyResourceBundle getPropertyBundle(final JrtURL jrtURL, final ResourceBundle parentBundle) throws IOException {

        try (final var inputStream = jrtURL.openStream())
        {
            return new PropertyResourceBundle(inputStream) {
                {
                    this.setParent(parentBundle /* (may be null) */);
                }
            };
        }
    }

    private static final ResourceBundle getNullBundle() {
        return new       ResourceBundle() {
            @Override
            protected Object handleGetObject(final String key) {
                return key;
            }
            @Override
            public Enumeration<String> getKeys() {
                return Collections.emptyEnumeration();
            }
        };
    }

    private static ContextMenu newContextMenu(final Control control) {

        final ContextMenu      contextMenu = new ContextMenu();

        control.setContextMenu(contextMenu);

        return                 contextMenu;
    }

    private static MenuItem addMenuItem(final ContextMenu parent, final ResourceBundle  bundle,  final Key titleKey, final Ability ability, final EventHandler<ActionEvent> handler) {
        return              addMenuItem(                  parent, titleKey.lookupString(bundle),                                   ability,                                 handler);
    }

    private static MenuItem addMenuItem(final ContextMenu parent,                                final String title, final Ability ability, final EventHandler<ActionEvent> handler) {

        final var                 child = new MenuItem(title);
        ;                         child.setDisable (ability.isDisabled());
        ;                         child.setOnAction(handler);

        if (parent != null) {
            parent.getItems().add(child);
        }

        return                    child;
    }

    private static SeparatorMenuItem addSeparator(final ContextMenu parent) {

        final var                 child = new SeparatorMenuItem();

        if (parent != null) {
            parent.getItems().add(child);
        }

        return                    child;
    }

    private void deleteSelectedText() {

        final var range = logTextArea.getSelection();

        if (range.getLength() == 0) {
            return;
        }
        final var                 text    = logTextArea.getText();
        final var                 newText = text.substring(0, range.getStart()) + text.substring(range.getEnd());

        logTextArea.setText      (newText);
        logTextArea.positionCaret(range.getStart());
    }

    private void changeLogThreshold() {

        final var header  =
                """
                Only messages with a Severity
                greater than or equal to the Threshold
                will be logged.
                """;

        final var choices = Arrays.asList(LogSeverity.values());

        final var chooser = new ChoiceDialog<LogSeverity>(LogSeverity.INFO, choices);
        ;         chooser.setTitle      ("Log Level");
        ;         chooser.setContentText("Threshold.:");
        ;         chooser.setHeaderText (header);

        ;         chooser.showAndWait().ifPresent(choice -> logTextArea.appendText("-> " + choice + '\n'));
    }

    public static void main(final String[] args) {
        launch(args);
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.