上下文菜单
本指南介绍如何使用 ShowContextMenuCallback 并显示自定义上下文菜单。
概述
您可以以编程方式拦截并处理用户在 BrowserView 可以通过编程方式拦截并处理用户在 BrowserView 中打开上下文菜单的任何尝试。这允许您实现自定义的上下文菜单 UI,或者直接在代码中选择合适的菜单项。

SWT 中 BrowserView 的自定义上下文菜单。
使用 ShowContextMenuCallback 回调来处理打开上下文菜单的操作:
browser.set(ShowContextMenuCallback.class, (params, tell) -> {
tell.close();
// 或:
// tell.select(...);
});
browser.register(ShowContextMenuCallback { params, tell ->
tell.close()
// 或:
// tell.select(...)
})
tell.close() 和 tell.select() 方法用于指示 Chromium “关闭”上下文菜单,可以是静默关闭,也可以是选择某个菜单项。你必须在准备“关闭”上下文菜单时调用其中之一。它们可以从任何线程调用,即使是在回调返回之后。
如果网页通过取消 contextmenu JavaScript 事件禁用了上下文菜单,则不会触发该回调。
JxBrowser 不支持显示 Chromium 的原生上下文菜单。
上下文菜单参数
ShowContextMenuCallback 的参数包含了在代码中选择正确菜单项或实现自定义上下文菜单所需的全部信息。
页面与 frame
上下文菜单参数提供了页面以及发生右键点击的具体 frame 的信息。例如,你可以用这些信息创建“Copy page URL(复制页面 URL)”菜单项:
// 页面 URL。
var pageUrl = params.pageUrl();
// frame 的 URL。
var frameUrl = params.frameUrl();
// frame 使用的字符集。
var frameCharset = params.frameCharset();
// 页面 URL。
val pageUrl = params.pageUrl()
// frame 的 URL。
val frameUrl = params.frameUrl()
// frame 使用的字符集。
val frameCharset = params.frameCharset()
位置
要在正确的位置显示上下文菜单,请使用 location() 方法,该方法返回相对于 BrowserView 的菜单位置:
var location = params.location();
var x = location.x();
var y = location.y();
val location = params.location()
val x = location.x()
val y = location.y()
您还可以获取相对于 frame 的位置:
var locationInFrame = params.locationInFrame();
val locationInFrame = params.locationInFrame()
查找光标下的元素
您可以确定用户右键点击的是哪个 DOM 节点,通过检查点击位置:
var locationInFrame = params.locationInFrame();
params.frame().ifPresent(frame -> {
var inspection = frame.inspect(locationInFrame);
inspection.node().ifPresent(node -> {
...
});
});
val locationInFrame = params.locationInFrame()
params.frame().ifPresent { frame ->
val inspection = frame.inspect(
locationInFrame!!
)
inspection.node().ifPresent { node ->
...
}
}
上下文菜单内容
当触发上下文菜单时,Chromium 会指示当前点击位置适用的菜单类别:
var contentTypes = params.contentType();
val contentTypes = params.contentType()
当在媒体元素上触发上下文菜单时,Chromium 会报告确切的媒体类型。当前可用的类型有:NONE、IMAGE、VIDEO、AUDIO、CANVAS、FILE、PLUGIN。
var mediaType = params.mediaType();
val mediaType = params.mediaType()
如果用户右键单击了链接,Chromium 会提供链接文本和 URL:
var linkUrl = params.linkUrl();
var linkText = params.linkText();
val linkUrl = params.linkUrl()
val linkText = params.linkText()
对于 <audio>、<video> 和 <img> 元素,Chromium 提供 src 属性的值:
var src = params.srcUrl();
val src = params.srcUrl()
如果用户右键单击了一段选中的文本,Chromium 会提供选中的文本:
var selectedText = params.selectedText();
val selectedText = params.selectedText()
拼写检查
当用户右键单击拼写错误的单词时,上下文菜单参数将包含拼写检查信息,使您能够用 Chromium 的建议替换该单词,并将该单词添加到自定义词典中。
var spellCheckMenu = params.spellCheckMenu();
var word = spellCheckMenu.misspelledWord();
if (!word.isEmpty()) {
// 获取建议的更正列表:
List<String> suggestions = spellCheckMenu.dictionarySuggestions();
// 将拼写错误的单词替换为某个更正项或任意其他字符串:
browser.replaceMisspelledWord(theRightSuggestion);
// 将拼写错误的单词添加到词典。
browser.profile().spellChecker().customDictionary().add(word);
}
val spellCheckMenu = params.spellCheckMenu()
val word = spellCheckMenu.misspelledWord()
if (word.isNotEmpty()) {
// 获取建议的更正列表:
val suggestions = spellCheckMenu.dictionarySuggestions()
// 将拼写错误的单词替换为某个更正项或任意其他字符串:
browser.replaceMisspelledWord(theRightSuggestion)
// 将拼写错误的单词添加到词典。
browser.profile().spellChecker().customDictionary().add(word)
}
此外,Chromium 还提供了一个本地化的“添加到词典(Add to dictionary)”标签,您可以在应用程序中使用它:
var label = params.spellCheckMenu().addToDictionaryMenuItemText();
val label = params.spellCheckMenu().addToDictionaryMenuItemText()
扩展菜单项
如果安装了 Chrome 扩展,库可能会提供这些扩展创建的额外上下文菜单项。使用 extensionMenuItems() 访问它们:
List<ContextMenuItem> items = params.extensionMenuItems();
for (var item : params.extensionMenuItems()) {
var text = item.text();
var enabled = item.isEnabled();
switch (item.type()) {
case ITEM -> {
// 针对标准项的逻辑。
}
case CHECKABLE_ITEM -> {
var checked = item.isChecked();
// 针对复选框项的逻辑。
}
case SUB_MENU -> {
var children = item.items();
// 针对子菜单及其子项的逻辑。
}
case SEPARATOR -> {
// 分隔符。
}
}
}
for (item in params.extensionMenuItems()) {
val text = item.text()
val enabled = item.isEnabled
when (item.type()) {
ContextMenuItemType.ITEM -> {
// 针对标准项的逻辑。
}
ContextMenuItemType.CHECKABLE_ITEM -> {
// 针对复选框项的逻辑。
val checked = item.isChecked
}
ContextMenuItemType.SUB_MENU -> {
// 针对子菜单及其子项的逻辑。
val children = item.items()
}
ContextMenuItemType.SEPARATOR -> {
// 分隔符。
}
else -> {}
}
}
使用 tell.select() 告诉 Chromium 选择其中一个扩展项:
browser.set(ShowContextMenuCallback.class, (params, tell) -> {
for (var item : params.extensionMenuItems()) {
if (item.text().equals("Some action")) {
tell.select(item);
return;
}
}
tell.close();
});
browser.register(ShowContextMenuCallback { params, tell ->
for (item in params.extensionMenuItems()) {
if (item.text() == "Some action") {
tell.select(item)
return@ShowContextMenuCallback
}
}
tell.close()
});
实现示例
在本节中,我们为 Swing、JavaFX 和 SWT 提供 ShowContextMenuCallback 的简单实现。
Swing
这是一个简单的 ShowContextMenuCallback 实现,它在 Swing 中显示上下文菜单。
在 HARDWARE_ACCELERATED 模式下,显示 JPopupMenu 之前请调用 JPopupMenu.setDefaultLightWeightPopupEnabled(false)。否则,菜单将不可见。有关更多详细信息,请阅读关于硬件加速模式的限制的内容。

Swing 中 BrowserView 的自定义上下文菜单。
import com.teamdev.jxbrowser.browser.callback.ShowContextMenuCallback;
import com.teamdev.jxbrowser.menu.ContextMenuItem;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ContextMenuCallback implements ShowContextMenuCallback {
private final BrowserView browserView;
public ContextMenuCallback(BrowserView browserView) {
this.browserView = browserView;
}
@Override
public void on(Params params, Action tell) {
SwingUtilities.invokeLater(() -> {
var copyPageUrl = createCopyUrlItem(params, tell);
var spellCheckMenu = createSpellCheckMenu(params, tell);
var extensionMenu = addExtensionMenu(params.extensionMenuItems(), tell);
var contextMenu = new JPopupMenu();
contextMenu.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
tell.close();
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
}
});
contextMenu.add(copyPageUrl);
contextMenu.addSeparator();
spellCheckMenu.forEach(contextMenu::add);
contextMenu.addSeparator();
extensionMenu.forEach(contextMenu::add);
var location = params.location();
contextMenu.show(browserView, location.x(), location.y());
});
}
private List<JMenuItem> createSpellCheckMenu(Params params, Action tell) {
var result = new ArrayList<JMenuItem>();
var spellCheck = params.spellCheckMenu();
var word = spellCheck.misspelledWord();
if (word.isEmpty()) {
return Collections.emptyList();
}
var addToDictionary = new JMenuItem(spellCheck.addToDictionaryMenuItemText());
addToDictionary.addActionListener(e -> {
var dictionary = params.browser().profile().spellChecker().customDictionary();
dictionary.add(word);
tell.close();
});
result.add(addToDictionary);
var suggestionsMenu = new JMenu("Suggestions");
spellCheck.dictionarySuggestions().forEach(suggestion -> {
var item = new JMenuItem(suggestion);
item.addActionListener(e -> {
params.browser().replaceMisspelledWord(suggestion);
tell.close();
});
suggestionsMenu.add(item);
});
result.add(suggestionsMenu);
return result;
}
private JMenuItem createCopyUrlItem(Params params, Action tell) {
var copyItem = new JMenuItem("Copy page URL");
copyItem.addActionListener(e -> {
var pageUrl = params.pageUrl();
var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(new StringSelection(pageUrl), null);
tell.close();
});
return copyItem;
}
private List<JComponent> addExtensionMenu(List<ContextMenuItem> items, Action tell) {
var result = new ArrayList<JComponent>();
items.forEach(item -> {
switch (item.type()) {
case ITEM -> {
var menuItem = new JMenuItem(item.text());
menuItem.setEnabled(item.isEnabled());
menuItem.addActionListener(e -> {
tell.select(item);
});
result.add(menuItem);
}
case CHECKABLE_ITEM -> {
var menuItem = new JCheckBoxMenuItem(item.text());
menuItem.setEnabled(item.isEnabled());
menuItem.setSelected(item.isChecked());
menuItem.addActionListener(e -> {
tell.select(item);
});
result.add(menuItem);
}
case SUB_MENU -> {
var subMenu = new JMenu(item.text());
subMenu.setEnabled(item.isEnabled());
var subMenuItems = addExtensionMenu(item.items(), tell);
subMenuItems.forEach(subMenu::add);
result.add(subMenu);
}
case SEPARATOR -> {
result.add(new JSeparator());
}
}
});
return result;
}
}
SWT
这是一个简单的 ShowContextMenuCallback实现,它在 SWT 中显示上下文菜单。由于 SWT 小部件的原生特性,上下文菜单在两种渲染模式下均可见。

SWT 中 BrowserView 的自定义上下文菜单。
import com.teamdev.jxbrowser.browser.callback.ShowContextMenuCallback;
import com.teamdev.jxbrowser.menu.ContextMenuItem;
import com.teamdev.jxbrowser.view.swt.BrowserView;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import java.util.List;
import java.util.function.Consumer;
public class ContextMenuCallback implements ShowContextMenuCallback {
private final BrowserView browserView;
private Consumer<Action> menuAction;
public ContextMenuCallback(BrowserView browserView) {
this.browserView = browserView;
}
@Override
public void on(Params params, Action tell) {
// 默认情况下关闭菜单。
menuAction = Action::close;
Display.getDefault().asyncExec(() -> {
var contextMenu = new Menu(browserView);
createCopyUrlItem(contextMenu, params, tell);
new MenuItem(contextMenu, SWT.SEPARATOR);
createSpellCheckMenu(contextMenu, params, tell);
new MenuItem(contextMenu, SWT.SEPARATOR);
addExtensionMenu(contextMenu, params.extensionMenuItems(), tell);
contextMenu.addListener(SWT.Hide, e -> {
// 当菜单隐藏时调用此事件:无论是用户选择了菜单项,还是点击外部取消菜单。
//
// 由于此事件总是在 SWT.Selection 之前发生,我们还不知道用户选择了哪个菜单项。
// 因此,我们延迟执行,让 SWT.Selection 处理器先执行。
e.display.asyncExec(() -> {
menuAction.accept(tell);
});
});
var location = params.location();
var point = browserView.toDisplay(location.x(), location.y());
contextMenu.setLocation(point.x, point.y);
contextMenu.setVisible(true);
});
}
private void createSpellCheckMenu(Menu parent, Params params, Action tell) {
var spellCheck = params.spellCheckMenu();
var word = spellCheck.misspelledWord();
if (word.isEmpty()) {
return;
}
var addToDictionary = new MenuItem(parent, SWT.PUSH);
addToDictionary.setText(spellCheck.addToDictionaryMenuItemText());
addToDictionary.addListener(SWT.Selection, e -> {
var dictionary = params.browser().profile().spellChecker().customDictionary();
dictionary.add(word);
});
var suggestionsMenu = new Menu(parent);
var suggestionsCascade = new MenuItem(parent, SWT.CASCADE);
suggestionsCascade.setText("Suggestions");
suggestionsCascade.setMenu(suggestionsMenu);
spellCheck.dictionarySuggestions().forEach(suggestion -> {
var item = new MenuItem(suggestionsMenu, SWT.PUSH);
item.setText(suggestion);
item.addListener(SWT.Selection, e -> {
params.browser().replaceMisspelledWord(suggestion);
});
});
}
private void createCopyUrlItem(Menu parent, Params params, Action tell) {
var copyItem = new MenuItem(parent, SWT.PUSH);
copyItem.setText("Copy page URL");
copyItem.addListener(SWT.Selection, e -> {
var pageUrl = params.pageUrl();
var clipboard = new Clipboard(Display.getDefault());
var transfer = TextTransfer.getInstance();
clipboard.setContents(new Object[]{pageUrl}, new Transfer[]{transfer});
clipboard.dispose();
});
}
private void addExtensionMenu(Menu parent, List<ContextMenuItem> items, Action tell) {
items.forEach(item -> {
switch (item.type()) {
case ITEM -> {
var menuItem = new MenuItem(parent, SWT.PUSH);
menuItem.setText(item.text());
menuItem.setEnabled(item.isEnabled());
menuItem.addListener(SWT.Selection, e -> {
menuAction = action -> action.select(item);
});
}
case CHECKABLE_ITEM -> {
var menuItem = new MenuItem(parent, SWT.CHECK);
menuItem.setText(item.text());
menuItem.setEnabled(item.isEnabled());
menuItem.setSelection(item.isChecked());
menuItem.addListener(SWT.Selection, e -> {
menuAction = action -> action.select(item);
});
}
case SUB_MENU -> {
var subMenu = new Menu(parent);
var cascade = new MenuItem(parent, SWT.CASCADE);
cascade.setMenu(subMenu);
cascade.setText(item.text());
cascade.setEnabled(item.isEnabled());
addExtensionMenu(subMenu, item.items(), tell);
}
case SEPARATOR -> new MenuItem(parent, SWT.SEPARATOR);
}
});
}
}
JavaFX
这是一个简单的 ShowContextMenuCallback 实现,它在 JavaFX 中显示上下文菜单。
在 HARDWARE_ACCELERATED 模式下,菜单将不可见。有关详细信息,请阅读关于硬件加速模式的限制的内容。

JavaFX 中 BrowserView 的自定义上下文菜单。
import com.teamdev.jxbrowser.browser.callback.ShowContextMenuCallback;
import com.teamdev.jxbrowser.menu.ContextMenuItem;
import com.teamdev.jxbrowser.view.javafx.BrowserView;
import javafx.application.Application;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.ClipboardContent;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.emptyList;
import static javafx.application.Platform.runLater;
import static javafx.scene.input.Clipboard.getSystemClipboard;
public class ContextMenuCallback implements ShowContextMenuCallback {
private final BrowserView browserView;
private boolean menuAlreadyClosed;
public ContextMenuCallback(BrowserView browserView) {
this.browserView = browserView;
}
@Override
public void on(Params params, Action tell) {
menuAlreadyClosed = false;
runLater(() -> {
var copyPageUrl = createCopyUrlItem(params, tell);
var spellCheckMenu = createSpellCheckMenu(params, tell);
var extensionMenu = addExtensionMenu(params.extensionMenuItems(), tell);
var contextMenu = new ContextMenu();
contextMenu.setAutoHide(true);
contextMenu.setOnHiding(e -> {
if (!menuAlreadyClosed) {
tell.close();
}
});
var menuItems = contextMenu.getItems();
menuItems.add(copyPageUrl);
menuItems.add(new SeparatorMenuItem());
menuItems.addAll(spellCheckMenu);
menuItems.add(new SeparatorMenuItem());
menuItems.addAll(extensionMenu);
var location = params.location();
var screenPoint = browserView.localToScreen(location.x(), location.y());
contextMenu.show(browserView, screenPoint.getX(), screenPoint.getY());
});
}
private List<MenuItem> createSpellCheckMenu(Params params, Action tell) {
var result = new ArrayList<MenuItem>();
var spellCheck = params.spellCheckMenu();
var word = spellCheck.misspelledWord();
if (word.isEmpty()) {
return emptyList();
}
var addToDictionary = new MenuItem(spellCheck.addToDictionaryMenuItemText());
addToDictionary.setOnAction(e -> {
var dictionary = params.browser().profile().spellChecker().customDictionary();
dictionary.add(word);
menuAlreadyClosed = true;
tell.close();
});
result.add(addToDictionary);
var suggestionsMenu = new Menu("Suggestions");
spellCheck.dictionarySuggestions().forEach(suggestion -> {
var item = new MenuItem(suggestion);
item.setOnAction(e -> {
params.browser().replaceMisspelledWord(suggestion);
menuAlreadyClosed = true;
tell.close();
});
suggestionsMenu.getItems().add(item);
});
result.add(suggestionsMenu);
return result;
}
private MenuItem createCopyUrlItem(Params params, Action tell) {
var copyItem = new MenuItem("Copy page URL");
copyItem.setOnAction(e -> {
var pageUrl = params.pageUrl();
var content = new ClipboardContent();
content.putString(pageUrl);
getSystemClipboard().setContent(content);
menuAlreadyClosed = true;
tell.close();
});
return copyItem;
}
private List<MenuItem> addExtensionMenu(List<ContextMenuItem> items, Action tell) {
var result = new ArrayList<MenuItem>();
items.forEach(item -> {
switch (item.type()) {
case ITEM -> {
var menuItem = new MenuItem(item.text());
menuItem.setDisable(!item.isEnabled());
menuItem.setOnAction(e -> {
menuAlreadyClosed = true;
tell.select(item);
});
result.add(menuItem);
}
case CHECKABLE_ITEM -> {
var menuItem = new CheckMenuItem(item.text());
menuItem.setDisable(!item.isEnabled());
menuItem.setSelected(item.isChecked());
menuItem.setOnAction(e -> {
menuAlreadyClosed = true;
tell.select(item);
});
result.add(menuItem);
}
case SUB_MENU -> {
var subMenu = new Menu(item.text());
subMenu.setDisable(!item.isEnabled());
var subMenuItems = addExtensionMenu(item.items(), tell);
subMenu.getItems().addAll(subMenuItems);
result.add(subMenu);
}
case SEPARATOR -> result.add(new SeparatorMenuItem());
}
});
return result;
}
}