using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; using System.Text.Json; namespace anjianjingling.Browser { /// /// 内嵌 WebView2 命令桥:把脚本命令同步封送到 UI 线程执行。 /// 脚本引擎在后台线程,WebView2 必须在 UI 线程访问。 /// public sealed class WebViewBridge { private readonly WebView2 _web; private const int DefaultTimeoutMs = 30000; public WebViewBridge(WebView2 web) { _web = web; } private T Invoke(Func> func, int timeoutMs = DefaultTimeoutMs) { var tcs = new TaskCompletionSource(); _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 func, int timeoutMs = DefaultTimeoutMs) { Invoke(async () => { await func(); return true; }, timeoutMs); } /// ExecuteScriptAsync 返回的是 JSON 字符串,解码为可读文本。 private static string DecodeJsResult(string json) { if (json == "null" || string.IsNullOrEmpty(json)) return ""; try { var el = JsonSerializer.Deserialize(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(); 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(); 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); }); } } }