151 lines
5.0 KiB
C#
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);
|
|
});
|
|
}
|
|
}
|
|
}
|