Files
2026-10-24 13:02:19 +08:00

151 lines
5.0 KiB
C#

using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
using System.Text.Json;
namespace anjianjingling.Browser
{
/// <summary>
/// 内嵌 WebView2 命令桥:把脚本命令同步封送到 UI 线程执行。
/// 脚本引擎在后台线程,WebView2 必须在 UI 线程访问。
/// </summary>
public sealed class WebViewBridge
{
private readonly WebView2 _web;
private const int DefaultTimeoutMs = 30000;
public WebViewBridge(WebView2 web) { _web = web; }
private T Invoke<T>(Func<Task<T>> func, int timeoutMs = DefaultTimeoutMs)
{
var tcs = new TaskCompletionSource<T>();
_web.BeginInvoke(async () =>
{
try { tcs.TrySetResult(await func()); }
catch (Exception ex) { tcs.TrySetException(ex); }
});
if (!tcs.Task.Wait(timeoutMs))
throw new TimeoutException("浏览器操作超时");
return tcs.Task.Result;
}
private void InvokeAction(Func<Task> func, int timeoutMs = DefaultTimeoutMs)
{
Invoke(async () => { await func(); return true; }, timeoutMs);
}
/// <summary>ExecuteScriptAsync 返回的是 JSON 字符串,解码为可读文本。</summary>
private static string DecodeJsResult(string json)
{
if (json == "null" || string.IsNullOrEmpty(json)) return "";
try
{
var el = JsonSerializer.Deserialize<JsonElement>(json);
return el.ValueKind == JsonValueKind.String
? el.GetString() ?? ""
: el.GetRawText();
}
catch { return json; }
}
private static string Js(string code) => JsonSerializer.Serialize(code);
// ===================== 命令 =====================
public void Open(string url)
{
Invoke(async () =>
{
var nav = new TaskCompletionSource<bool>();
void Handler(object? s, CoreWebView2NavigationCompletedEventArgs e)
{
_web.NavigationCompleted -= Handler;
nav.TrySetResult(e.IsSuccess);
}
_web.NavigationCompleted += Handler;
_web.CoreWebView2.Navigate(url);
bool ok = await nav.Task;
return ok;
});
}
public string ExecJs(string js)
=> DecodeJsResult(Invoke(() => _web.CoreWebView2.ExecuteScriptAsync(js)));
public bool Click(string selector)
{
string js = $$"""
(()=>{
const el=document.querySelector({{Js(selector)}});
if(!el) return false;
el.scrollIntoView({block:'center'});
el.dispatchEvent(new MouseEvent('mousedown',{bubbles:true}));
el.dispatchEvent(new MouseEvent('mouseup',{bubbles:true}));
el.click();
return true;
})()
""";
return ExecJs(js) == "True";
}
public bool Type(string selector, string text)
{
string js = $$"""
(()=>{
const el=document.querySelector({{Js(selector)}});
if(!el) return false;
const proto = el.tagName==='TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto,'value').set;
setter.call(el, {{Js(text)}});
el.dispatchEvent(new Event('input',{bubbles:true}));
el.dispatchEvent(new Event('change',{bubbles:true}));
return true;
})()
""";
return ExecJs(js) == "True";
}
private bool Exists(string selector)
=> ExecJs($"!!document.querySelector({Js(selector)})") == "True";
public bool WaitFor(string selector, int timeoutMs)
{
var tcs = new TaskCompletionSource<bool>();
Task.Run(async () =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
while (sw.ElapsedMilliseconds < timeoutMs)
{
try { if (Exists(selector)) { tcs.TrySetResult(true); return; } }
catch { }
await Task.Delay(200);
}
tcs.TrySetResult(false);
});
return tcs.Task.Wait(timeoutMs + 5000) && tcs.Task.Result;
}
public string GetText(string selector)
{
string js = $$"""
(()=>{
const el=document.querySelector({{Js(selector)}});
if(!el) return null;
return el.value!==undefined ? el.value : (el.innerText||el.textContent);
})()
""";
return ExecJs(js);
}
public void Shot(string path)
{
InvokeAction(async () =>
{
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
await using var fs = new FileStream(path, FileMode.Create);
await _web.CoreWebView2.CapturePreviewAsync(
CoreWebView2CapturePreviewImageFormat.Png, fs);
});
}
}
}