feat(browser): 添加 JCEF 浏览器窗口功能

- 新增 BrowserWindow 类实现浏览器窗口功能- 添加 WindowOperation、WindowOperationHandler 和 WindowRegistry 类用于窗口操作管理
- 在 MainApplication 中使用 JCEF 创建主窗口
- 实现了窗口创建、关闭等基本操作
This commit is contained in:
tzdwindows 7
2025-03-22 21:05:20 +08:00
parent 4ce244579f
commit d116e80cbf
8 changed files with 1192 additions and 0 deletions

View File

@@ -0,0 +1,369 @@
package com.axis.innovators.box.browser;
import com.axis.innovators.box.tools.FolderCreator;
import org.cef.*;
import org.cef.browser.*;
import org.cef.callback.CefQueryCallback;
import org.cef.handler.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Consumer;
public class BrowserWindow extends JFrame {
private final String windowId;
private CefApp cefApp;
private CefClient client;
private CefBrowser browser;
private final Component browserComponent;
private final String htmlPath;
private static boolean isInitialized = false;
private WindowOperationHandler operationHandler;
private static Thread cefThread;
private CefMessageRouter msgRouter;
public static class Builder {
private String windowId;
private String title = "JCEF Window";
private Dimension size = new Dimension(800, 600);
private WindowOperationHandler operationHandler;
private String htmlPath;
private Image icon;
public Builder(String windowId) {
this.windowId = windowId;
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder size(int width, int height) {
this.size = new Dimension(width, height);
return this;
}
public Builder operationHandler(WindowOperationHandler handler) {
this.operationHandler = handler;
return this;
}
public Builder icon(Image icon) {
this.icon = icon;
return this;
}
public BrowserWindow build() {
if (this.htmlPath == null || this.htmlPath.isEmpty()) {
throw new IllegalArgumentException("HTML paths cannot be empty");
}
File htmlFile = new File(this.htmlPath);
if (!htmlFile.exists()) {
throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
}
return new BrowserWindow(this);
}
public Builder htmlPath(String path) {
this.htmlPath = path;
return this;
}
}
private BrowserWindow(Builder builder) {
this.windowId = builder.windowId;
this.htmlPath = builder.htmlPath;
this.operationHandler = builder.operationHandler;
if (builder.icon != null) {
setIconImage(builder.icon);
}
try {
this.browserComponent = initializeCef(builder);
if (operationHandler != null) {
setupMessageHandlers(operationHandler);
}
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
//System.exit(1);
throw new RuntimeException(e);
}
}
private Component initializeCef(Builder builder) throws MalformedURLException {
if (!isInitialized && CefApp.getState() != CefApp.CefAppState.INITIALIZED) {
try {
// 1. 初始化CEF设置禁用多窗口
CefSettings settings = new CefSettings();
settings.windowless_rendering_enabled = false;
settings.javascript_flags = "--expose-gc";
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_VERBOSE;
String subprocessPath = FolderCreator.getLibraryFolder() + "/jcef/lib/win64/jcef_helper.exe";
// 验证子进程路径
if (!new File(subprocessPath).exists()) {
throw new IllegalStateException("jcef_helper.exe not found at: " + subprocessPath);
}
settings.browser_subprocess_path = subprocessPath;
System.out.println("Subprocess Path: " + settings.browser_subprocess_path);
// 2. 创建CefApp和Client严格单例
cefApp = CefApp.getInstance(settings);
client = cefApp.createClient();
client.addDisplayHandler(new CefDisplayHandler (){
@Override
public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {
}
@Override
public void onTitleChange(CefBrowser browser, String title) {
}
@Override
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {
}
@Override
public boolean onTooltip(CefBrowser browser, String text) {
return false;
}
@Override
public void onStatusMessage(CefBrowser browser, String value) {
}
@Override
public boolean onConsoleMessage(
CefBrowser browser,
CefSettings.LogSeverity level,
String message,
String source,
int line
) {
// 格式化输出到 Java 控制台
String log = String.format(
"[Browser Console] %s %s (Line %d) -> %s",
getLogLevelSymbol(level),
source,
line,
message
);
System.out.println(log);
return false;
}
@Override
public boolean onCursorChange(CefBrowser browser, int cursorType) {
return false;
}
private String getLogLevelSymbol(CefSettings.LogSeverity level) {
switch (level) {
case LOGSEVERITY_ERROR: return "";
case LOGSEVERITY_WARNING: return "⚠️";
case LOGSEVERITY_DEFAULT: return "🐞";
default: return "";
}
}
});
// 3. 拦截所有新窗口(关键修复点!)
//client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
// @Override
// public boolean onBeforePopup(CefBrowser browser,
// CefFrame frame, String target_url, String target_frame_name) {
// return true; // 返回true表示拦截弹窗
// }
//});
Thread.currentThread().setName("BrowserRenderThread");
// 4. 加载HTML
String fileUrl = new File(htmlPath).toURI().toURL().toString();
System.out.println("Loading HTML from: " + fileUrl);
// 5. 创建浏览器组件(直接添加到内容面板)
browser = client.createBrowser(fileUrl, false, false);
Component browserComponent = browser.getUIComponent();
browser.executeJavaScript("console.log('Java -> HTML 消息测试')",null,2);
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
config.jsQueryFunction = "javaQuery";// 定义方法
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
// 6. 配置窗口布局(确保只添加一次)
SwingUtilities.invokeLater(() -> {
getContentPane().removeAll(); // 清空已有组件
getContentPane().setLayout(new BorderLayout());
// 透明拖拽层(仅顶部可拖拽)
JPanel dragPanel = new JPanel(new BorderLayout());
dragPanel.setOpaque(false);
JPanel titleBar = new JPanel();
titleBar.setOpaque(false);
titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
final Point[] dragStart = new Point[1];
titleBar.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
dragStart[0] = e.getPoint();
}
@Override
public void mouseReleased(MouseEvent e) {
dragStart[0] = null;
}
});
titleBar.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (dragStart[0] != null) {
Point curr = e.getLocationOnScreen();
setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
}
}
});
dragPanel.add(titleBar, BorderLayout.NORTH);
getContentPane().add(dragPanel, BorderLayout.CENTER);
getContentPane().add(browserComponent, BorderLayout.CENTER);
// 7. 窗口属性设置
setTitle(builder.title);
setSize(builder.size); // 直接设置尺寸避免pack()计算错误
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// 8. 资源释放
addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
browser.close(true); // 强制关闭浏览器
client.dispose();
cefApp.dispose();
isInitialized = false;
}
});
setVisible(true);
isInitialized = true;
});
return browserComponent;
} catch (Exception e) {
e.printStackTrace();
isInitialized = false;
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
System.exit(1);
}
} else {
SwingUtilities.invokeLater(() -> {
dispose();
});
}
return null;
}
public static void printStackTrace() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (int i = 2; i < stackTrace.length; i++) {
StackTraceElement element = stackTrace[i];
System.out.println(element.getClassName() + "." + element.getMethodName() +
"(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
":" + element.getLineNumber() + ")");
}
}
@Override
public void setVisible(boolean b) {
super.setVisible(b);
}
public Component getBrowserComponent() {
return browserComponent;
}
private void setupMessageHandlers(WindowOperationHandler handler) {
if (client != null) {
msgRouter = CefMessageRouter.create();
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
@Override
public boolean onQuery(CefBrowser browser,
CefFrame frame,
long queryId,
String request,
boolean persistent,
CefQueryCallback callback) {
if (request.startsWith("system:")) {
String[] parts = request.split(":");
String operation = parts.length >= 2 ? parts[1] : null;
String targetWindow = parts.length > 2 ? parts[2] : null;
handler.handleOperation(
new WindowOperation(operation, targetWindow, callback) // [!code ++]
);
return true;
}
if (request.startsWith("java-response:")) {
String[] parts = request.split(":");
String requestId = parts[1];
String responseData = parts.length > 2 ? parts[2] : "";
Consumer<String> handler = WindowRegistry.getInstance().getCallback(requestId);
if (handler != null) {
handler.accept(responseData);
callback.success("");
} else {
callback.failure(-1, "无效的请求ID");
}
return true;
}
return false;
}
}, true);
client.addMessageRouter(msgRouter);
}
}
public String getWindowId() {
return windowId;
}
/**
* 获取消息路由器
* @return 消息路由器
*/
public CefMessageRouter getMsgRouter() {
return msgRouter;
}
/**
* 获取浏览器对象
* @return 浏览器对象
*/
public CefBrowser getBrowser() {
return browser;
}
public void closeWindow() {
SwingUtilities.invokeLater(() -> {
if (browser != null) {
browser.close(true);
}
dispose();
cefApp.dispose();
WindowRegistry.getInstance().unregisterWindow(windowId);
});
}
}

View File

@@ -0,0 +1,34 @@
package com.axis.innovators.box.browser;
import javax.swing.*;
/**
* 这是一个简单的示例程序用于展示如何使用JCEF来创建一个简单的浏览器窗口。
*/
public class MainApplication {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
//if (!CefApp.startup(args)) {
// System.out.println("Startup initialization failed!");
// return;
//}
WindowRegistry.getInstance().createNewWindow("main", builder ->
builder.title("Axis Innovators Box")
.size(1280, 720)
.htmlPath("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\java\\com\\axis\\innovators\\box\\browser\\main.html")
.operationHandler(createOperationHandler())
.build()
);
});
}
private static WindowOperationHandler createOperationHandler() {
return new WindowOperationHandler.Builder()
.withDefaultOperations()
.onOperation("我是注册的指令", s -> {
})
.build();
}
}

View File

@@ -0,0 +1,27 @@
package com.axis.innovators.box.browser;
import org.cef.callback.CefQueryCallback;
public class WindowOperation {
private final String type;
private final String targetWindow;
private final CefQueryCallback callback;
public WindowOperation(String type, String targetWindow, CefQueryCallback callback) { // [!code ++]
this.type = type;
this.targetWindow = targetWindow;
this.callback = callback; // [!code ++]
}
public String getType() {
return type;
}
public String getTargetWindow() {
return targetWindow;
}
public CefQueryCallback getCallback() {
return callback;
}
}

View File

@@ -0,0 +1,94 @@
package com.axis.innovators.box.browser;
import javax.swing.*;
import java.awt.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* @author tzdwindows 7
*/
public class WindowOperationHandler {
private final WindowRegistry registry;
private final Map<String, Consumer<String>> operations;
private final Component attachedComponent;
public static class Builder {
private WindowRegistry registry = WindowRegistry.getInstance();
private final Map<String, Consumer<String>> operations = new ConcurrentHashMap<>();
private Component attachedComponent;
public Builder attachTo(Component component) {
this.attachedComponent = component;
return this;
}
public Builder withDefaultOperations() {
this.operations.put("open", target -> {
registry.getWindow(target).setVisible(true);
});
this.operations.put("close", target -> {
if (target != null) {
System.out.println("Close window: " + target);
registry.unregisterWindow(target);
} else if (attachedComponent != null) {
Window window = SwingUtilities.getWindowAncestor(attachedComponent);
if (window instanceof BrowserWindow) {
((BrowserWindow) window).closeWindow();
}
}
});
return this;
}
public Builder onOperation(String operation, Consumer<String> handler) {
this.operations.put(operation, handler);
return this;
}
public WindowOperationHandler build() {
return new WindowOperationHandler(this);
}
private void handleOpen(String targetWindow) {
registry.createNewWindow(targetWindow, builder ->
builder.title("New Window")
);
}
private void handleClose(String targetWindow) {
if (targetWindow != null) {
registry.unregisterWindow(targetWindow);
} else {
handleCurrentWindowClose();
}
}
private void handleCurrentWindowClose() {
if (attachedComponent != null) {
BrowserWindow currentWindow = (BrowserWindow)
SwingUtilities.getWindowAncestor(attachedComponent);
if (currentWindow != null) {
registry.unregisterWindow(currentWindow.getWindowId());
}
}
}
}
private WindowOperationHandler(Builder builder) {
this.registry = builder.registry;
this.attachedComponent = builder.attachedComponent;
this.operations = new ConcurrentHashMap<>(builder.operations);
}
public void handleOperation(WindowOperation operation) {
Consumer<String> handler = operations.get(operation.getType());
if (handler != null) {
handler.accept(operation.getTargetWindow());
operation.getCallback().success("操作成功: " + operation.getType());
} else {
operation.getCallback().failure(-1, "未定义的操作: " + operation.getType());
}
}
}

View File

@@ -0,0 +1,51 @@
package com.axis.innovators.box.browser;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Consumer;
public class WindowRegistry {
private static WindowRegistry instance;
private final ConcurrentMap<String, BrowserWindow> windows =
new ConcurrentHashMap<>();
private final Map<String, Consumer<String>> callbacks = new ConcurrentHashMap<>();
private WindowRegistry() {}
public static synchronized WindowRegistry getInstance() {
if (instance == null) {
instance = new WindowRegistry();
}
return instance;
}
public void registerWindow(BrowserWindow window) {
windows.put(window.getWindowId(), window);
}
public void registerCallback(String requestId, Consumer<String> handler) {
callbacks.put(requestId, handler);
}
public Consumer<String> getCallback(String requestId) {
return callbacks.remove(requestId);
}
public void unregisterWindow(String windowId) {
BrowserWindow window = windows.remove(windowId);
if (window != null) {
window.closeWindow();
}
}
public BrowserWindow getWindow(String windowId) {
return windows.get(windowId);
}
public void createNewWindow(String windowId, Consumer<BrowserWindow.Builder> config) {
BrowserWindow.Builder builder = new BrowserWindow.Builder(windowId);
config.accept(builder);
BrowserWindow window = builder.build();
registerWindow(window);
}
}

View File

@@ -0,0 +1,13 @@
<script>
// 必须定义此函数以接收 Java 消息
function javaMessageReceived(requestId, message) {
console.log("[HTML] 收到 Java 消息:", requestId, message);
// 示例:将消息转为大写并返回
const response = message.toUpperCase();
window.cefQuery({
request: 'java-response:' + requestId + ':' + response,
onSuccess: function() {},
onFailure: function() {}
});
}
</script>