需求描述:
jxbrowser将Web浏览器组件集成到Java Swing / AWT / JavaFX应用程序中,因为公司项目需求是需要将网页内容放到JavaFx中。因为常用的浏览器在打开文件弹窗时都会记录上次打开的目录,下次打开时直接可以访问到想要的目录文件,增加了用户体验度。
而内嵌到javaFx后,jxbrowser没有记录文件访问目录,每次都会打开计算机根目录。
市面上关于jxbrowser的技术文档相当少,所以只能参考官方提供的参考文档和API.
注:本文使用的jxbrowser的版本是7.19,不同版本之间会有差异。
解决思路
根据jxbrowser提供的文档,jxbrowser可以监听文件触发文件框,那么我们就可以深入触发机制,把触发时的指向目录进行更改,前提是我们要保存每次访问的文件目录。
但是jxbrowser默认情况下,所有弹出窗口都被禁止,也就意味着打开新页面后,原有页面的监听会失效,所以需要对新弹出的窗口再次监听。
思路实现
jxbrowser的javaFX实现
import com.teamdev.jxbrowser.browser.Browser;
import com.teamdev.jxbrowser.browser.callback.*;
import com.teamdev.jxbrowser.browser.callback.OpenFolderCallback;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.view.javafx.BrowserView;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED;
/**
* This example demonstrates how to initialize Chromium, create a browser instance
* (equivalent of the Chromium tab), embed a JavaFX BrowserView component into JavaFX
* scene to display content of the loaded web page, load the required web page.
*/
public final class Main extends Application {
@Override
public void start(Stage primaryStage) {
//jxbriwser需要进行注册后才能使用
System.setProperty("jxbrowser.license.key", "注册码****");
// Initialize Chromium.
Engine engine = Engine.newInstance(HARDWARE_ACCELERATED);
Browser browser = engine.newBrowser();
// Load the required web page.
//放入浏览器网页地址
browser.navigation().loadUrl("http://localhost:8080/");
// Create and embed JavaFX BrowserView component to display web content.
BrowserView view = BrowserView.newInstance(browser);
Scene scene = new Scene(new BorderPane(view), 1920, 1080);
primaryStage.setTitle("***");
primaryStage.setScene(scene);
primaryStage.show();
primaryStage.setMaximized(true);
//单文件监听
browser.set(OpenFilesCallback.class, (params, tell) -> {
DefaultOpenFilesCallback d = new DefaultOpenFilesCallback(view);
d.on(params, tell);
});
//多文件监听
browser.set(OpenFileCallback.class, (params, tell) -> {
DefaultOpenFileCallback d = new DefaultOpenFileCallback(view);
d.on(params, tell);
});
//新弹窗监听
browser.set(OpenPopupCallback.class, (params) -> {
// Browser popup = params.popupBrowser();
DefaultOpenPopupCallback defaultOpenPopupCallback = new DefaultOpenPopupCallback();
defaultOpenPopupCallback.on(params);
return DefaultOpenPopupCallback.Response.proceed();
});
// Shutdown Chromium and release allocated resources.
primaryStage.setOnCloseRequest(event -> engine.close());
}
}
因为我们更改jxbrowser的触发文件框的目录,又因为jxbrowser提供的jar包里面的方法不能更改,所以我们需要重新实现这些方法。
- html中单文件触发文件框方法重新实现
DefaultOpenFileCallback触发文件框方法重新实现,实现方式是把每次打开文件的目录保存到本地的cache.txt中,每次打开触发文件框时都会读取cache.txt中的目录地址。此方法只能监听前端单文件上传实现。
代码实现:
import com.teamdev.jxbrowser.browser.callback.OpenFileCallback;
import com.teamdev.jxbrowser.view.javafx.BrowserView;
import javafx.application.Platform;
import javafx.stage.FileChooser;
import java.io.*;
public class DefaultOpenFileCallback extends DefaultCallback implements OpenFileCallback {
public DefaultOpenFileCallback(BrowserView parent) {
super(parent);
}
public void on(Params params, Action tell) {
Platform.runLater(() -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Choose File");
fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("All Files", "*.*"));
File cacheFile = new File("cache.txt");
if (cacheFile.exists()) {
try (InputStream inputStream = new FileInputStream(cacheFile)) {
byte[] bytes = new byte[(int) cacheFile.length()];
inputStream.read(bytes);
File directory = new File(new String(bytes));
if (directory.exists()) {
fileChooser.setInitialDirectory(directory);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
}
this.window().ifPresent((window) -> {
FileChoosers.setExtensionFilters(fileChooser, params.filterDescription(), params.acceptableExtensions());
File file = fileChooser.showOpenDialog(window);
if (file != null) {
try (OutputStream outputStream = new FileOutputStream(cacheFile)) {
byte[] bytes = file.getParent().getBytes();
outputStream.write(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
if (file != null) {
tell.open(file.toPath());
} else {
tell.cancel();
}
});
});
}
}
- html中多文件触发实现
DefaultOpenFilesCallback的方法的重新实现。在此方法中与单文件实现逻辑相同
代码如下:
import com.teamdev.jxbrowser.browser.callback.OpenFilesCallback;
import com.teamdev.jxbrowser.view.javafx.BrowserView;
import javafx.application.Platform;
import javafx.stage.FileChooser;
import java.io.*;
import java.nio.file.Path;
import java.util.List;
public class DefaultOpenFilesCallback extends DefaultCallback implements OpenFilesCallback {
DefaultOpenFilesCallback(BrowserView parent) {
super(parent);
}
public void on(Params params, Action tell) {
Platform.runLater(() -> {
FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("All Files", "*.*"));
File cacheFile = new File("cache.txt");
if (cacheFile.exists()) {
try (InputStream inputStream = new FileInputStream(cacheFile)) {
byte[] bytes = new byte[(int) cacheFile.length()];
inputStream.read(bytes);
File directory = new File(new String(bytes));
if (directory.exists()) {
fileChooser.setInitialDirectory(directory);
}
} catch (IOException e) {
e.printStackTrace();
}
}
this.window().ifPresent((window) -> {
FileChoosers.setExtensionFilters(fileChooser, params.filterDescription(), params.acceptableExtensions());
List<File> selectedFiles = fileChooser.showOpenMultipleDialog(window);
if (selectedFiles != null) {
try (OutputStream outputStream = new FileOutputStream(cacheFile)) {
byte[] bytes = selectedFiles.get(selectedFiles.size() - 1).getParent().getBytes();
outputStream.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
tell.open((Path[])selectedFiles.stream().map(File::toPath).toArray(Path[]::new));
} else {
tell.cancel();
}
});
});
}
}
因为重写后,需要原有的继承类,但是jar包外不能访问jar包中final的方法与类,所以单独拿出来
- FileChoosers类
import java.util.List;
import java.util.stream.Collectors;
import javafx.collections.ObservableList;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
final class FileChoosers {
private FileChoosers() {
}
static void setExtensionFilters(FileChooser fileChooser, String filterDescription, List<String> extensions) {
ObservableList<ExtensionFilter> extensionFilters = fileChooser.getExtensionFilters();
ExtensionFilter allFilesFilter;
if (!extensions.isEmpty()) {
allFilesFilter = new ExtensionFilter(filterDescription, withPrefix(extensions));
extensionFilters.add(allFilesFilter);
fileChooser.setSelectedExtensionFilter(allFilesFilter);
}
allFilesFilter = new ExtensionFilter("All files", new String[]{"*.*"});
extensionFilters.add(allFilesFilter);
}
private static List<String> withPrefix(List<String> extensions) {
return (List)extensions.stream().map((e) -> {
return "*." + e;
}).collect(Collectors.toList());
}
}
- DefaultCallback类的实现
import com.teamdev.jxbrowser.view.WidgetHolder;
import com.teamdev.jxbrowser.view.javafx.BrowserView;
import java.util.Optional;
import javafx.scene.Scene;
import javafx.stage.Window;
abstract class DefaultCallback extends WidgetHolder<BrowserView> {
DefaultCallback(BrowserView parent) {
super(parent);
}
protected Optional<Window> window() {
Scene scene = ((BrowserView)this.widget()).getScene();
return scene != null ? Optional.ofNullable(scene.getWindow()) : Optional.empty();
}
}
到此为止,html中的文件上传都会保存上次的的访问目录,且每次都是上次打开的目录。
但是遇到了新的问题,就是html中如果有新页面打开,监听就会失效,因为我们之前写的只是监听上个页面的文件触发。
所以尝试了很久,,如果新页面要覆盖原有页面时只需要添加
browser.set(CreatePopupCallback.class, params -> {
browser.navigation().loadUrl(params.targetUrl());
return CreatePopupCallback.Response.suppress();
});
就可以实现原有的监听成功。
但是如果新页面需要单独打开,且不覆盖原有页面时,那么此时新的brower就会生成,那么我们需要做的就是实现监听新页面的触发事件。
官方提供的DefaultOpenPopupCallback并不能满足我们的业务需求,所以我们需要重写,加入文件触发的监听事件。
public final class DefaultOpenPopupCallback implements OpenPopupCallback {
private static final int DEFAULT_POPUP_WIDTH = 800;
private static final int DEFAULT_POPUP_HEIGHT = 600;
@Override
public Response on(Params paramss) {
Browser browser = paramss.popupBrowser();
runLater(() -> {
BrowserView view = BrowserView.newInstance(browser);
Stage stage = new Stage();
StackPane root = new StackPane();
Scene scene = new Scene(root);
root.getChildren().add(view);
stage.setScene(scene);
updateBounds(stage, paramss.initialBounds());
stage.setOnCloseRequest(event -> browser.close());
browser.on(TitleChanged.class, event ->
runLater(() -> stage.setTitle(event.title()))
);
browser.on(BrowserClosed.class, event ->
runLater(stage::close)
);
browser.on(UpdateBoundsRequested.class, event ->
runLater(() -> updateBounds(stage, event.bounds()))
);
browser.set(OpenFilesCallback.class, (params, tell) -> {
DefaultOpenFilesCallback d = new DefaultOpenFilesCallback(view);
d.on(params, tell);
});
browser.set(OpenFileCallback.class, (params, tell) -> {
DefaultOpenFileCallback d = new DefaultOpenFileCallback(view);
d.on(params, tell);
});
stage.show();
});
return Response.proceed();
}
private static void updateBounds(Stage stage, Rect bounds) {
if (bounds.size().isEmpty()) {
stage.setWidth(DEFAULT_POPUP_WIDTH);
stage.setHeight(DEFAULT_POPUP_HEIGHT);
} else {
stage.setX(bounds.origin().x());
stage.setY(bounds.origin().y());
stage.setWidth(bounds.size().width());
stage.setHeight(bounds.size().height());
}
}
到此,需求解决。
参考官方文档与api地址:https://jxbrowser-support.teamdev.com/docs/guides/popups.html