我想问一个小问题。 事实上,我想自定义在文本区域或文本字段中右键单击时出现的菜单。 我的目标是通过添加我想要的按钮来保留基本菜单(复制、粘贴、剪切...)。
我发现这篇文章解释了如何做到这一点: 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+ 来做到这一点,我将非常感激
由于模块化,您的代码将无法在 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
经过长时间的编程,我找到了一种“扩展”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。
正如 @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);
}
}