Files
bansonic_beta_main/Assets/scripts/WebView/WebViewLauncher.cs
T

764 lines
26 KiB
C#

using System.Diagnostics;
using System.IO;
using UnityEngine;
using System;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Text.RegularExpressions;
using Debug = UnityEngine.Debug;
public class WebViewLauncher : MonoBehaviour
{
public static WebViewLauncher Instance { get; private set; }
[Header("Web Settings")]
public string startUrl = "https://www.baidu.com";
public Vector2 windowSize = new Vector2(960, 600);
public Vector2 windowPosition = new Vector2(100, 100);
[Header("Editor Only Path")]
public string editorExeRoot = @"E:\Unity\ban_total\fdBrowser";
[Header("H5 Folder Paths")]
public string h5EditorPath = @"E:\Unity\ban_total\ban_test\Bansonic_Data\StreamingAssets\h5LG";
public string officialDocsPath = "_officialDocs";
public string officialGamesPath = "_officialGames";
public string thirdPartyGamesPath = "_thirdPartyGames";
private Process webProcess;
// Event invoked when web process main window handle becomes available
public event Action WebViewReady;
[Header("Control Buttons (optional)")]
public Button refreshButton; // 刷新当前webview页面
public Button closeButton; // 关闭webview
public Button restartButton; // 重启webview
public Button backButton; // 返回
public Button forwardButton; // 前进
public Button startRestartButton; // 新增:根据运行状态启动或重启 webview2
public Button forceKillButton; // 强制结束进程按钮
public Button hideWindowButton; // 隐藏webview窗口
public Button showWindowButton; // 显示webview窗口
public Button toggleTextButton; // 新增:控制文本显示/隐藏的按钮
[Header("Text to Toggle")]
public Text controlText; // 要控制显示/隐藏的文本
[Header("Autostart")]
public UnityEngine.UI.Toggle autostartToggle; // UI toggle to control auto-start
private bool autostart = true;
private const string PREF_AUTOSTART = "WebView_AutoStart";
[Header("Homepage / URL UI")]
public Button homepageButton; // 返回主页
public InputField urlInputField; // 显示/输入当前 url
[Tooltip("Homepage URL (can be file:// or http(s) or local path)")]
public string homepageUrl = "";
[Header("Go button")]
public Button goButton; // 点击跳转,和回车效果一致
void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// wire up optional UI buttons
if (refreshButton != null) refreshButton.onClick.AddListener(RefreshWebView);
if (closeButton != null) closeButton.onClick.AddListener(CloseWebView);
if (restartButton != null) restartButton.onClick.AddListener(RestartWebView);
if (backButton != null) backButton.onClick.AddListener(GoBackWebView);
if (forwardButton != null) forwardButton.onClick.AddListener(GoForwardWebView);
if (startRestartButton != null) startRestartButton.onClick.AddListener(OnStartRestartButtonClicked);
if (forceKillButton != null) forceKillButton.onClick.AddListener(OnForceKillClicked);
if (hideWindowButton != null) hideWindowButton.onClick.AddListener(OnHideWindowClicked);
if (showWindowButton != null) showWindowButton.onClick.AddListener(OnShowWindowClicked);
if (toggleTextButton != null) toggleTextButton.onClick.AddListener(ToggleTextVisibility);
if (homepageButton != null) homepageButton.onClick.AddListener(OnHomepageClicked);
if (urlInputField != null) urlInputField.onEndEdit.AddListener(OnUrlInputEndEdit);
if (goButton != null) goButton.onClick.AddListener(OnGoButtonClicked);
// Load autostart preference and bind toggle
autostart = PlayerPrefs.GetInt(PREF_AUTOSTART, 1) == 1;
if (autostartToggle != null)
{
autostartToggle.isOn = autostart;
autostartToggle.onValueChanged.AddListener(OnAutostartToggleChanged);
}
// set default homepage if empty
if (string.IsNullOrEmpty(homepageUrl))
{
#if UNITY_EDITOR
string candidate = Path.Combine(h5EditorPath, "_officialDocs", "openings", "index.html");
if (File.Exists(candidate))
homepageUrl = new Uri(candidate).AbsoluteUri;
else
homepageUrl = startUrl;
#else
string candidate = Path.Combine(Application.streamingAssetsPath, "h5LG", "_officialDocs", "openings", "index.html");
if (File.Exists(candidate))
homepageUrl = new Uri(candidate).AbsoluteUri;
else
homepageUrl = startUrl;
#endif
}
}
void Start()
{
// Ensure H5 root exists
string h5Folder = GetH5FolderPath();
if (!Directory.Exists(h5Folder))
{
Directory.CreateDirectory(h5Folder);
Debug.Log("Created H5 folder: " + h5Folder);
}
// Ensure controlText is visible by default
if (controlText != null)
{
controlText.gameObject.SetActive(true);
Debug.Log("Control text set to visible by default");
}
// Prefer explicit openings index as startup page when available
string preferred = Path.Combine(h5Folder, "_officialDocs", "openings", "index.html");
if (File.Exists(preferred))
{
startUrl = new Uri(preferred).AbsoluteUri;
Debug.Log("Using preferred startup page: " + startUrl);
// show initial url in input field
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(startUrl);
if (autostart) LaunchWebView();
return;
}
// 扫描 H5 文件夹
var h5SubFolders = ScanH5Folders(h5Folder);
if (h5SubFolders.Count > 0)
{
Debug.Log("Found H5 folders: " + string.Join(", ", h5SubFolders));
string selectedFolder = h5SubFolders[0]; // 选择第一个
string candidatePath = Path.Combine(h5Folder, selectedFolder, "index.html");
startUrl = new Uri(candidatePath).AbsoluteUri; // ensure proper file:/// URI
Debug.Log("Selected H5 folder: " + selectedFolder + ", URL: " + startUrl);
}
else
{
Debug.LogWarning("No H5 subfolders with index.html found in: " + h5Folder);
}
// show initial url in input field if available (use shortened display for local files that are indexed)
if (urlInputField != null)
{
urlInputField.text = GetDisplayUrl(startUrl);
}
if (autostart) LaunchWebView();
}
void OnDestroy()
{
KillWebView();
// remove listeners
if (refreshButton != null) refreshButton.onClick.RemoveListener(RefreshWebView);
if (closeButton != null) closeButton.onClick.RemoveListener(CloseWebView);
if (restartButton != null) restartButton.onClick.RemoveListener(RestartWebView);
if (backButton != null) backButton.onClick.RemoveListener(GoBackWebView);
if (forwardButton != null) forwardButton.onClick.RemoveListener(GoForwardWebView);
if (startRestartButton != null) startRestartButton.onClick.RemoveListener(OnStartRestartButtonClicked);
if (forceKillButton != null) forceKillButton.onClick.RemoveListener(OnForceKillClicked);
if (hideWindowButton != null) hideWindowButton.onClick.RemoveListener(OnHideWindowClicked);
if (showWindowButton != null) showWindowButton.onClick.RemoveListener(OnShowWindowClicked);
if (toggleTextButton != null) toggleTextButton.onClick.RemoveListener(ToggleTextVisibility);
if (autostartToggle != null) autostartToggle.onValueChanged.RemoveListener(OnAutostartToggleChanged);
if (homepageButton != null) homepageButton.onClick.RemoveListener(OnHomepageClicked);
if (urlInputField != null) urlInputField.onEndEdit.RemoveListener(OnUrlInputEndEdit);
if (goButton != null) goButton.onClick.RemoveListener(OnGoButtonClicked);
}
void Update()
{
// if input field focused and Enter pressed -> navigate
if (urlInputField != null && urlInputField.isFocused && (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)))
{
string text = urlInputField.text;
if (!string.IsNullOrEmpty(text))
{
OpenUrl(text);
// remove focus
EventSystem.current?.SetSelectedGameObject(null);
urlInputField.DeactivateInputField();
}
}
// clicking elsewhere should unfocus input field (end edit)
if (Input.GetMouseButtonDown(0) && urlInputField != null && urlInputField.isFocused)
{
var sel = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null;
if (sel != urlInputField.gameObject)
{
urlInputField.DeactivateInputField();
EventSystem.current?.SetSelectedGameObject(null);
}
}
}
void OnGoButtonClicked()
{
if (urlInputField == null) return;
string text = urlInputField.text;
if (!string.IsNullOrEmpty(text))
{
OpenUrl(text);
EventSystem.current?.SetSelectedGameObject(null);
urlInputField.DeactivateInputField();
}
}
public void KillWebView()
{
if (webProcess != null && !webProcess.HasExited)
{
try
{
webProcess.Kill();
}
catch (Exception e)
{
Debug.LogWarning("KillWebView failed: " + e.Message);
}
webProcess = null;
}
}
void LaunchWebView()
{
string exePath = GetExePath();
Debug.Log("WebView exe path: " + exePath);
if (!File.Exists(exePath))
{
Debug.LogError("fdBrowser.exe not found: " + exePath);
return;
}
// 初始参数可以给 0,后续由 PanelFollower 接管
string args = $"0 0 800 600 \"{startUrl}\"";
Debug.Log($"Launching: {exePath} {args}");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = exePath,
Arguments = args,
UseShellExecute = true, // 改为 true 以确保 exe 能启动
WorkingDirectory = Path.GetDirectoryName(exePath)
};
try
{
webProcess = Process.Start(psi);
Debug.Log("WebView2Host started successfully. Process ID: " + webProcess.Id);
// Start coroutine to wait until main window handle is ready
StartCoroutine(WaitForMainWindowHandle());
}
catch (System.Exception e)
{
Debug.LogError("Failed to start WebView2Host: " + e.Message + "\nPath: " + exePath + "\nArgs: " + args);
}
}
IEnumerator WaitForMainWindowHandle()
{
if (webProcess == null)
yield break;
float timeout = 5f; // seconds
float elapsed = 0f;
while (elapsed < timeout)
{
try
{
webProcess.Refresh();
if (webProcess.MainWindowHandle != IntPtr.Zero)
{
Debug.Log($"WebViewLauncher: MainWindowHandle ready: 0x{webProcess.MainWindowHandle.ToInt64():X}");
WebViewReady?.Invoke();
yield break;
}
}
catch (Exception e)
{
Debug.LogWarning("WebViewLauncher.WaitForMainWindowHandle exception: " + e.Message);
}
yield return null;
elapsed += Time.deltaTime;
}
Debug.LogWarning("WebViewLauncher: timeout waiting for main window handle");
// still invoke event so follower can attempt positioning later
WebViewReady?.Invoke();
}
/// <summary>
/// 从外部请求打开一个 url。如果 webview 没有运行则启动;如果已运行则尝试通过 WM_COPYDATA 发送 url 给已有窗口,失败则重启。
/// 传入的 url 建议使用本地文件路径或以 file:// 开头的路径。
/// </summary>
/// <param name="url"></param>
public void OpenUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
Debug.LogWarning("OpenUrl called with empty url");
return;
}
// Normalize to absolute file:// or keep as web URL
string final;
try
{
if (IsLikelyWebUrl(url))
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var parsed) || string.IsNullOrEmpty(parsed.Scheme))
{
final = "http://" + url;
}
else
{
final = parsed.AbsoluteUri;
}
}
else if (url.StartsWith("localhost::"))
{
// Map localhost::display back to actual file URI
string relativePath = url.Substring("localhost::".Length).Replace('/', Path.DirectorySeparatorChar);
string h5Folder = GetH5FolderPath();
string fullPath = Path.Combine(h5Folder, relativePath);
final = new Uri(fullPath).AbsoluteUri;
}
else
{
// Treat as local file path
string abs = Path.GetFullPath(url);
final = new Uri(abs).AbsoluteUri; // yields file:///C:/...
}
}
catch (Exception e)
{
Debug.LogWarning("OpenUrl: failed to normalize url '" + url + "' : " + e.Message);
final = url;
}
Debug.Log($"OpenUrl requested. original='{url}', normalized='{final}'");
startUrl = final;
// update input field display
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(final);
// if process not running, launch; otherwise try to send url to existing window
if (webProcess == null || webProcess.HasExited)
{
Debug.Log("WebView process not running. Launching with URL: " + final);
LaunchWebView();
return;
}
try
{
Debug.Log($"WebView process running. PID={webProcess.Id}, MainWindowHandle=0x{webProcess.MainWindowHandle.ToInt64():X}");
}
catch { }
// 尝试通过 WM_COPYDATA 将 URL 发送给已有进程窗口(需要 fdBrowser 实现接收处理)
bool sent = WebViewWin32.TrySendUrlToWindow(webProcess, final);
Debug.Log($"TrySendUrlToWindow returned: {sent}");
if (sent)
{
Debug.Log("Sent URL to existing WebView process: " + final);
// Notify followers that webview content may have changed and they should reapply positioning
WebViewReady?.Invoke();
return;
}
// 发送失败,退回到重启进程打开 URL 的方式
Debug.Log("Failed to send URL to existing process, restarting to open: " + final);
KillWebView();
LaunchWebView();
// after relaunch, WaitForMainWindowHandle will invoke WebViewReady when handle ready
}
// Try to send a simple command string to the webview process via WM_COPYDATA. Returns true when SendMessage returned non-zero.
bool TrySendCommand(string cmd)
{
if (webProcess == null || webProcess.HasExited)
{
Debug.LogWarning("TrySendCommand: webProcess not running");
return false;
}
bool sent = WebViewWin32.TrySendUrlToWindow(webProcess, cmd);
Debug.Log($"TrySendCommand('{cmd}') returned: {sent}");
return sent;
}
// UI control methods
public void RefreshWebView()
{
// Try sending a refresh command; fallback to restarting to refresh
if (TrySendCommand("CMD:REFRESH"))
{
Debug.Log("Sent REFRESH command to webview");
return;
}
Debug.Log("REFRESH command not supported, restarting webview to refresh");
KillWebView();
LaunchWebView();
}
public void CloseWebView()
{
Debug.Log("Closing webview process");
KillWebView();
}
public void RestartWebView()
{
Debug.Log("Restarting webview process");
KillWebView();
LaunchWebView();
}
public void GoBackWebView()
{
if (TrySendCommand("CMD:BACK"))
{
Debug.Log("Sent BACK command to webview");
return;
}
Debug.LogWarning("BACK command not supported by webview");
}
public void GoForwardWebView()
{
if (TrySendCommand("CMD:FORWARD"))
{
Debug.Log("Sent FORWARD command to webview");
return;
}
Debug.LogWarning("FORWARD command not supported by webview");
}
void OnHomepageClicked()
{
// Always navigate to StreamingAssets/h5LG/_officialDocs/openings/index.html
string candidate = Path.Combine(Application.streamingAssetsPath, "h5LG", "_officialDocs", "openings", "index.html");
if (!File.Exists(candidate))
{
Debug.LogWarning("Homepage index not found at: " + candidate + ". Falling back to homepageUrl or startUrl.");
if (!string.IsNullOrEmpty(homepageUrl))
{
LaunchWebViewWithUrl(homepageUrl);
}
else
{
LaunchWebViewWithUrl(startUrl);
}
return;
}
string fileUri = new Uri(candidate).AbsoluteUri;
LaunchWebViewWithUrl(fileUri);
// OpenUrl will update the input field using GetDisplayUrl(final). Ensure display shows the shortened localhost form.
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(fileUri);
}
void OnUrlInputEndEdit(string text)
{
if (string.IsNullOrEmpty(text)) return;
OpenUrl(text);
}
void OnStartRestartButtonClicked()
{
if (webProcess == null || webProcess.HasExited)
{
Debug.Log("WebView process not running. Starting webview.");
StartWebView();
}
else
{
Debug.Log("WebView process running. Restarting webview.");
RestartWebView();
}
}
void OnForceKillClicked()
{
Debug.Log("Force killing webview process");
KillWebView();
}
void OnHideWindowClicked()
{
if (webProcess == null || webProcess.HasExited)
{
Debug.LogWarning("HideWindow: webview process not running");
return;
}
bool ok = WebViewWin32.TryShowWindow(webProcess, false);
Debug.Log("HideWindow clicked, result: " + ok);
}
void OnShowWindowClicked()
{
if (webProcess == null || webProcess.HasExited)
{
Debug.LogWarning("ShowWindow: webview process not running");
return;
}
bool ok = WebViewWin32.TryShowWindow(webProcess, true);
Debug.Log("ShowWindow clicked, result: " + ok);
}
/// <summary>
/// 切换控制文本的显示/隐藏状态
/// </summary>
void ToggleTextVisibility()
{
if (controlText == null)
{
Debug.LogWarning("ToggleTextVisibility: controlText is not assigned");
return;
}
bool isCurrentlyActive = controlText.gameObject.activeSelf;
controlText.gameObject.SetActive(!isCurrentlyActive);
Debug.Log($"Text visibility toggled: {(isCurrentlyActive ? "hidden" : "shown")}");
}
public void StartWebView()
{
LaunchWebView();
}
public void StopWebView()
{
KillWebView();
}
void OnAutostartToggleChanged(bool value)
{
autostart = value;
PlayerPrefs.SetInt(PREF_AUTOSTART, autostart ? 1 : 0);
PlayerPrefs.Save();
Debug.Log("Autostart toggled: " + autostart);
}
string GetExePath()
{
#if UNITY_EDITOR
return Path.Combine(editorExeRoot, "fdBrowser.exe");
#else
// 优先使用 StreamingAssets 路径
string streamingPath = Path.Combine(Application.streamingAssetsPath, "fdBrowser", "fdBrowser.exe");
if (File.Exists(streamingPath))
{
Debug.Log("Using StreamingAssets path: " + streamingPath);
return streamingPath;
}
// 回退到游戏根目录
string gameRoot = Path.GetDirectoryName(Application.dataPath);
string fallbackPath = Path.Combine(gameRoot, "fdBrowser", "fdBrowser.exe");
Debug.Log("Using fallback path: " + fallbackPath);
return fallbackPath;
#endif
}
string GetH5FolderPath()
{
#if UNITY_EDITOR
return h5EditorPath;
#else
return Path.Combine(Application.streamingAssetsPath, "h5LG");
#endif
}
System.Collections.Generic.List<string> ScanH5Folders(string folderPath)
{
var result = new System.Collections.Generic.List<string>();
if (!Directory.Exists(folderPath))
{
Debug.LogWarning("H5 folder does not exist: " + folderPath);
return result;
}
Debug.Log("Scanning H5 folders in: " + folderPath);
string[] targetFolders = { officialDocsPath, officialGamesPath, thirdPartyGamesPath };
foreach (string folderName in targetFolders)
{
string subFolderPath = Path.Combine(folderPath, folderName);
if (!Directory.Exists(subFolderPath))
{
Directory.CreateDirectory(subFolderPath);
Debug.Log("Created subfolder: " + subFolderPath);
}
string indexPath = Path.Combine(subFolderPath, "index.html");
if (!File.Exists(indexPath))
{
File.WriteAllText(indexPath, "<html><body><h1>" + folderName + "</h1></html>");
Debug.Log("Created index.html in: " + subFolderPath);
}
if (File.Exists(indexPath))
{
result.Add(folderName);
Debug.Log("Found H5 folder with index.html: " + folderName);
}
}
Debug.Log("Total H5 folders found: " + result.Count);
return result;
}
/// <summary>
/// 给 PanelFollower 调用
/// </summary>
public bool TryUpdateWebViewRect(int x, int y, int w, int h)
{
if (webProcess == null || webProcess.HasExited)
return false;
return WebViewWin32.TrySetWindowRect(webProcess, x, y, w, h);
}
string GetDisplayUrl(string url)
{
if (string.IsNullOrEmpty(url))
return url;
try
{
Uri uri = new Uri(url);
if (uri.Scheme == Uri.UriSchemeFile)
{
string localPath = uri.LocalPath;
string h5Folder = GetH5FolderPath();
if (localPath.StartsWith(h5Folder, StringComparison.OrdinalIgnoreCase))
{
string relativePath = localPath.Substring(h5Folder.Length).TrimStart(Path.DirectorySeparatorChar);
return $"localhost::{relativePath.Replace(Path.DirectorySeparatorChar, '/')}";
}
}
}
catch (Exception e)
{
Debug.LogWarning("GetDisplayUrl: failed to parse url '" + url + "' : " + e.Message);
}
return url;
}
bool IsLikelyWebUrl(string url)
{
return Regex.IsMatch(url, @"^(https?:\/\/|www\.|[a-zA-Z0-9-]+\.[a-zA-Z]{2,})");
}
string NormalizeToAbsolute(string url)
{
if (string.IsNullOrEmpty(url))
{
Debug.LogWarning("NormalizeToAbsolute called with empty url");
return url;
}
try
{
if (Uri.TryCreate(url, UriKind.Absolute, out var parsed) && parsed.IsAbsoluteUri)
{
return parsed.AbsoluteUri;
}
if (url.StartsWith("localhost::"))
{
// Map localhost::display back to actual file URI
string relativePath = url.Substring("localhost::".Length).Replace('/', Path.DirectorySeparatorChar);
string h5Folder = GetH5FolderPath();
string fullPath = Path.Combine(h5Folder, relativePath);
return new Uri(fullPath).AbsoluteUri;
}
// Treat as local file path
string abs = Path.GetFullPath(url);
return new Uri(abs).AbsoluteUri; // yields file:///C:/...
}
catch (Exception e)
{
Debug.LogWarning("NormalizeToAbsolute: failed to normalize url '" + url + "' : " + e.Message);
return url;
}
}
void LaunchWebViewWithUrl(string url)
{
string finalUrl = NormalizeToAbsolute(url);
Debug.Log($"LaunchWebViewWithUrl requested. original='{url}', normalized='{finalUrl}'");
startUrl = finalUrl;
// update input field display
if (urlInputField != null)
urlInputField.text = GetDisplayUrl(finalUrl);
// if process not running, launch; otherwise try to send url to existing window
if (webProcess == null || webProcess.HasExited)
{
Debug.Log("WebView process not running. Launching with URL: " + finalUrl);
LaunchWebView();
return;
}
try
{
Debug.Log($"WebView process running. PID={webProcess.Id}, MainWindowHandle=0x{webProcess.MainWindowHandle.ToInt64():X}");
}
catch { }
// 尝试通过 WM_COPYDATA 将 URL 发送给已有进程窗口(需要 fdBrowser 实现接收处理)
bool sent = WebViewWin32.TrySendUrlToWindow(webProcess, finalUrl);
Debug.Log($"TrySendUrlToWindow returned: {sent}");
if (sent)
{
Debug.Log("Sent URL to existing WebView process: " + finalUrl);
// Notify followers that webview content may have changed and they should reapply positioning
WebViewReady?.Invoke();
return;
}
// 发送失败,退回到重启进程打开 URL 的方式
Debug.Log("Failed to send URL to existing process, restarting to open: " + finalUrl);
KillWebView();
LaunchWebView();
// after relaunch, WaitForMainWindowHandle will invoke WebViewReady when handle ready
}
}