使用 DotNetBrowser 在 C# 中自动操作网页时,通常会通过它的 DOM API 来完成:找到输入框、设置其值,再点击按钮。但很多网页不会对这种方式设置的值作出响应。它们会等待用户实际输入或点击所产生的事件。例如,只有用户在输入框中输入内容后,按钮才会启用。
模拟键盘和鼠标输入可以解决这个问题。浏览器接收到的事件与真实键盘和鼠标产生的事件相同,因此网页无法区分两者。这种方式适用于自动化操作,例如在桌面应用中测试 Web 界面,或者填写无法修改的旧版 Web 应用中的表单。
本文将创建一个简单的 WPF 应用:在网页中输入一项待办任务,然后点击添加按钮。我们会分别使用 DotNetBrowser 和 CefSharp 实现这个应用,方便对照代码。最后,还会介绍如何使用 WebView2 完成同样的操作。
准备工作
开始之前,请准备好:
- Windows 10 或更高版本。
- .NET 8 SDK 或更高版本。
- DotNetBrowser 许可证密钥。试用密钥可免费使用 30 天。
- Visual C++ Redistributable 2022(x64)。CefSharp 需要它,DotNetBrowser 则不需要。
创建网页
首先,我们需要一个要自动操作的网页:一个包含文本框、Add 按钮和列表的愿望清单。创建一个文件夹存放两个项目,例如 BucketList,并在其中创建 shared 文件夹。将下面的页面保存到 shared 文件夹,文件名为 bucket-list.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bucket list</title>
</head>
<body>
<h1>Bucket list</h1>
<input id="task" type="text" placeholder="What do you want to do?" autofocus>
<button id="add" disabled>Add</button>
<ul id="list"></ul>
<script>
const input = document.getElementById('task');
const addButton = document.getElementById('add');
const list = document.getElementById('list');
// 按钮由真实的输入事件启用,而不是通过设置 input.value 启用。
input.addEventListener('input', () => {
addButton.disabled = input.value.trim() === '';
});
addButton.addEventListener('click', () => {
const item = document.createElement('li');
item.textContent = input.value.trim();
list.appendChild(item);
input.value = '';
addButton.disabled = true;
input.focus();
});
</script>
</body>
</html>
页面加载时,Add 按钮处于禁用状态;只有触发 input 事件后,按钮才会启用。点击按钮会将任务添加到列表中、清空输入框,并将光标重新移回输入框。

输入任何内容之前的页面。
创建应用
接下来,我们创建两个应用。它们都是只有一个窗口的 WPF 应用:上方显示浏览器,下方有两个按钮。Type task(输入任务) 用于在输入框中输入文字,Click Add(点击添加) 用于点击网页上的按钮。
DotNetBrowser 项目
DotNetBrowser 是一款基于 Chromium 的 .NET 网页视图控件。首先,在 BucketList 文件夹中创建项目,并添加 DotNetBrowser.Wpf 程序包。该程序包会自动引入其他 DotNetBrowser 程序包:
dotnet new wpf -o DotNetBrowserApp
dotnet add DotNetBrowserApp package DotNetBrowser.Wpf
然后,在 DotNetBrowserApp.csproj 中添加以下内容,将网页复制到输出目录:
<ItemGroup>
<None Include="..\shared\bucket-list.html" Link="bucket-list.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
替换 MainWindow.xaml 中的内容:
<Window x:Class="DotNetBrowserApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpf="clr-namespace:DotNetBrowser.Wpf;assembly=DotNetBrowser.Wpf"
Title="Bucket list — DotNetBrowser" Height="480" Width="640"
Closed="Window_Closed">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
HorizontalAlignment="Center" Margin="8">
<Button Content="Type task" Width="120" Margin="4"
Click="TypeTask_Click" />
<Button Content="Click Add" Width="120" Margin="4"
Click="ClickAdd_Click" />
</StackPanel>
<wpf:BrowserView Name="browserView" />
</DockPanel>
</Window>
替换 MainWindow.xaml.cs 中的内容。构造函数会创建 Engine 和 Browser,将 Browser 附加到视图,并加载网页。XAML 中引用了两个尚未实现的按钮事件处理程序,因此在接下来的部分添加这些处理程序之前,项目还无法编译:
using System;
using System.IO;
using System.Windows;
using DotNetBrowser.Browser;
using DotNetBrowser.Engine;
namespace DotNetBrowserApp
{
public partial class MainWindow : Window
{
private readonly IEngine engine;
private readonly IBrowser browser;
public MainWindow()
{
engine = EngineFactory.Create(new EngineOptions.Builder
{
RenderingMode = RenderingMode.HardwareAccelerated,
LicenseKey = "<your_license_key>"
}.Build());
browser = engine.CreateBrowser();
InitializeComponent();
browserView.InitializeFrom(browser);
string page = Path.Combine(AppContext.BaseDirectory,
"bucket-list.html");
browser.Navigation.LoadUrl(page);
}
private void Window_Closed(object sender, EventArgs e)
{
browser.Dispose();
engine.Dispose();
}
}
}
这里将渲染模式设为硬件加速,这样 Chromium 就会直接在窗口中绘制页面。Engine 指南还介绍了另一种模式:离屏渲染。
将 <your_license_key> 替换为你的许可证密钥。关于如何通过许可证文件等其他方式设置密钥,请参阅许可证指南。
CefSharp 项目
接下来,我们用 CefSharp 实现相同的应用。CefSharp 是 Chromium Embedded Framework 的开源 .NET 封装库。在 BucketList 文件夹中创建第二个项目:
dotnet new wpf -o CefSharpApp
dotnet add CefSharpApp package CefSharp.Wpf.NETCore
CefSharp 会为不同架构提供不同的原生二进制文件,因此项目需要指定目标架构。在 CefSharpApp.csproj 中添加平台设置和网页文件:
<PropertyGroup>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
</PropertyGroup>
<ItemGroup>
<None Include="..\shared\bucket-list.html" Link="bucket-list.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
除了标出的几行之外,窗口与前面的相同。主要区别是使用 ChromiumWebBrowser 而不是 BrowserView,并且不需要 Closed 事件处理程序。替换 MainWindow.xaml 中的内容:
<Window x:Class="CefSharpApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cef="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
Title="Bucket list — CefSharp" Height="480" Width="640">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
HorizontalAlignment="Center" Margin="8">
<Button Content="Type task" Width="120" Margin="4"
Click="TypeTask_Click" />
<Button Content="Click Add" Width="120" Margin="4"
Click="ClickAdd_Click" />
</StackPanel>
<cef:ChromiumWebBrowser Name="browser" />
</DockPanel>
</Window>
CefSharp 会在创建第一个浏览器控件时启动 Chromium,并在应用退出时将其关闭,因此构造函数只需加载网页。替换 MainWindow.xaml.cs 中的内容:
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using CefSharp;
namespace CefSharpApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
string page = Path.Combine(AppContext.BaseDirectory,
"bucket-list.html");
browser.Address = new Uri(page).AbsoluteUri;
}
}
}
模拟键盘输入
窗口准备好后,我们先让 Type task 按钮发挥作用。键盘每输入一个字符,通常会产生三个事件:按键按下、字符输入和按键释放。网页会分别收到 keydown、keypress 和 keyup 事件,字符插入后还会触发 input 事件。要模拟键盘输入,我们也需要触发这三个事件。与通过 JavaScript 分发、isTrusted 属性为 false 的事件不同,这些事件是可信事件,因此网页会像处理真实输入一样处理它们。
在 DotNetBrowser 中,IBrowser.Keyboard 提供 KeyPressed、KeyTyped 和 KeyReleased 事件,每个事件都有一个 Raise() 方法。在 DotNetBrowser 项目的 MainWindow.xaml.cs 中添加以下命名空间:
using DotNetBrowser.Input.Keyboard;
using DotNetBrowser.Input.Keyboard.Events;
然后在类中添加事件处理程序和辅助方法:
private void TypeTask_Click(object sender, RoutedEventArgs e)
{
browser.Focus();
browser.MainFrame.Document.GetElementById("task").Focus();
TypeText(browser.Keyboard, "Learn to surf");
}
private static void TypeText(IKeyboard keyboard, string text)
{
foreach (char c in text)
{
// 字母、数字和空格的按键代码与其大写字符代码相同。
KeyCode key = (KeyCode)char.ToUpperInvariant(c);
string keyChar = c.ToString();
KeyModifiers modifiers = new KeyModifiers
{
ShiftDown = char.IsUpper(c)
};
keyboard.KeyPressed.Raise(new KeyPressedEventArgs
{
VirtualKey = key,
KeyChar = keyChar,
Modifiers = modifiers
});
keyboard.KeyTyped.Raise(new KeyTypedEventArgs
{
VirtualKey = key,
KeyChar = keyChar,
Modifiers = modifiers
});
keyboard.KeyReleased.Raise(new KeyReleasedEventArgs
{
VirtualKey = key,
Modifiers = modifiers
});
}
}
点击 WPF 按钮会使 Browser 失去焦点,因此事件处理程序会先将焦点还给 Browser。然后,它通过 DOM API 将焦点移到文本框,以防用户之前点击了网页上的其他位置。
按下和输入字符事件会分别通过 VirtualKey 和 KeyChar 传递按键和字符;释放事件只传递按键。对于字母、数字和空格,按键代码与对应大写字符的代码相同,因此直接转换类型即可。大写字母还需要设置 ShiftDown,这与真实键盘的输入方式一致。对于 @ 这样的符号,需要传入用户键盘布局中用于输入该符号的按键。例如,在美式键盘上,可以使用 KeyCode.Vk2 并设置 ShiftDown。
在 CefSharp 中,键盘事件通过 IBrowserHost 发送,事件类型分别是 RawKeyDown、Char 和 KeyUp。对于 RawKeyDown 和 KeyUp,WindowsKeyCode 表示按键代码;对于 Char,它表示字符代码。在 CefSharp 项目的 MainWindow.xaml.cs 中添加事件处理程序和辅助方法:
private async void TypeTask_Click(object sender, RoutedEventArgs e)
{
IBrowserHost host = browser.GetBrowserHost();
host.SendFocusEvent(true);
await browser.EvaluateScriptAsync(
"document.getElementById('task').focus()");
TypeText(host, "Learn to surf");
}
private static void TypeText(IBrowserHost host, string text)
{
foreach (char c in text)
{
// 字母、数字和空格的按键代码与其大写字符代码相同。
int keyCode = char.ToUpperInvariant(c);
CefEventFlags modifiers = char.IsUpper(c)
? CefEventFlags.ShiftDown
: CefEventFlags.None;
host.SendKeyEvent(new KeyEvent
{
Type = KeyEventType.RawKeyDown,
WindowsKeyCode = keyCode,
Modifiers = modifiers
});
host.SendKeyEvent(new KeyEvent
{
Type = KeyEventType.Char,
WindowsKeyCode = c,
Modifiers = modifiers
});
host.SendKeyEvent(new KeyEvent
{
Type = KeyEventType.KeyUp,
WindowsKeyCode = keyCode,
Modifiers = modifiers
});
}
}
两种实现中的辅助方法很相似。DotNetBrowser 将按键和字符分别存放在不同字段中,而 CefSharp 只有一个整数型字段,其含义取决于事件类型。两者的事件处理程序差异更大:CefSharp 没有 DOM API,因此处理程序需要通过 SendFocusEvent 将焦点还给 Browser,再用 JavaScript 将焦点移到文本框。如需了解两个库的更多区别,请参阅 选择 DotNetBrowser 还是 CefSharp 以及迁移指南。
模拟鼠标点击
接下来,让 Click Add 按钮发挥作用。一次鼠标点击包括移动鼠标、按下鼠标按钮和释放鼠标按钮,三者都发生在同一个位置。难点在于找到这个位置:也就是按钮在浏览器坐标系中的位置。
在 DotNetBrowser 中,可以通过 DOM API 找到元素,再使用 SimulateClick 扩展方法点击它。在 DotNetBrowser 项目的 MainWindow.xaml.cs 中添加以下命名空间:
using DotNetBrowser.Dom;
using DotNetBrowser.Input.Mouse;
using DotNetBrowser.Input.Mouse.Events;
然后添加事件处理程序:
private void ClickAdd_Click(object sender, RoutedEventArgs e)
{
IElement addButton = browser.MainFrame.Document.GetElementById("add");
browser.Mouse.SimulateClick(MouseButton.Left, addButton);
}
SimulateClick 会在元素中心触发 IMouse.Moved、IMouse.Pressed 和 IMouse.Released 事件,并在事件之间稍作停顿。如果已经知道坐标,可以使用接收 Point 的重载方法。如果要实现双击或拖动,就像模拟键盘输入那样,手动触发鼠标事件即可。Raise() 方法同样适用于 IBrowser.Mouse、IBrowser.Keyboard 和 IBrowser.Touch 上的所有事件,包括鼠标滚轮和触摸手势。更多示例请参阅 Browser 指南中的模拟用户输入。
在 CefSharp 中,我们先用脚本找到按钮中心的位置,再由 Browser 宿主在该位置发送鼠标事件。在 CefSharp 项目的 MainWindow.xaml.cs 中添加事件处理程序:
private async void ClickAdd_Click(object sender, RoutedEventArgs e)
{
// 没有 DOM API,因此需要通过 JavaScript 获取按钮的位置。
JavascriptResponse response = await browser.EvaluateScriptAsync(@"
(() => {
const rect = document.getElementById('add').getBoundingClientRect();
return [rect.x + rect.width / 2, rect.y + rect.height / 2];
})()");
List<object> center = (List<object>)response.Result;
int x = Convert.ToInt32(center[0]);
int y = Convert.ToInt32(center[1]);
IBrowserHost host = browser.GetBrowserHost();
MouseEvent mouseEvent = new MouseEvent(x, y, CefEventFlags.None);
host.SendMouseMoveEvent(mouseEvent, false);
host.SendMouseClickEvent(mouseEvent, MouseButtonType.Left, false, 1);
host.SendMouseClickEvent(mouseEvent, MouseButtonType.Left, true, 1);
}
EvaluateScriptAsync 会将 JavaScript 数组作为由装箱数值组成的 List<object> 返回。CefSharp 使用相对于视图左上角的坐标;对于主框架,getBoundingClientRect() 返回的正是这种坐标。每次调用 SendMouseClickEvent 都会发送一个事件:按下时,mouseUp 为 false;释放时,mouseUp 为 true。
运行应用
两个项目现在都已完成。在 BucketList 文件夹中运行其中一个项目:
dotnet run --project DotNetBrowserApp
dotnet run --project CefSharpApp

两个应用启动后的窗口。
点击 Type task:文本会出现在输入框中,Add 按钮也会启用,因为网页收到了 input 事件。然后点击 Click Add:任务会出现在列表中,输入框被清空,按钮再次变为禁用状态。

无需手动操作网页,任务也能添加到列表中。
包含两个项目的完整示例可在 GitHub 仓库中查看。
WebView2 怎么样?
WebView2 是微软基于 Edge 的网页视图控件,但它没有提供模拟键盘输入的 API。对于鼠标输入,它提供了 CoreWebView2CompositionController.SendMouseInput,但该方法只能在通过组合 API 承载 WebView2 时使用,而且 WPF 和 Windows Forms 控件并未公开这个方法。
因此,可以改用 Chrome DevTools Protocol(CDP)。这是调试工具用来控制 Chromium 的 JSON 接口。通过 CallDevToolsProtocolMethodAsync,可以调用任意协议方法,包括 Input.dispatchKeyEvent 和 Input.dispatchMouseEvent。调用时需要手动编写 JSON 参数。例如,模拟一次按键按下:
await webView.CoreWebView2.CallDevToolsProtocolMethodAsync(
"Input.dispatchKeyEvent",
@"{ ""type"": ""keyDown"", ""key"": ""L"", ""text"": ""L"" }");
这种方式可以实现目标,但每个事件都需要手动编写 JSON 字符串,而不是使用类型化对象。此外,您还需要像 CefSharp 示例那样,通过 ExecuteScriptAsync 和 getBoundingClientRect() 自行计算坐标。详情请参阅方法参考和 Input 域。
相比之下,DotNetBrowser 提供了类型化的键盘和鼠标事件,以及用于查找目标元素的 DOM API,具体用法如前面的代码所示。下面对比了这三种网页视图控件:
| CefSharp | WebView2 | DotNetBrowser | |
|---|---|---|---|
| 输入 | 使用原始按键代码调用 SendKeyEvent() | DevTools Protocol | 通过 Keyboard 事件调用 Raise() |
| 点击 | 在指定坐标调用 SendMouseClickEvent() | DevTools Protocol | 在 DOM 元素上调用 Mouse.SimulateClick() |
| 查找目标元素 | JavaScript | JavaScript | DOM API |
更多区别请参阅 选择 DotNetBrowser 还是 WebView2 以及 WebView2 迁移指南。
总结
本文分别使用 DotNetBrowser 和 CefSharp 创建了相同的 WPF 应用,并通过模拟输入在网页中输入文字、点击按钮。如果网页会对值的变化作出响应,可以使用 DOM API;如果网页依赖输入事件,则可以模拟用户输入。完整示例见 GitHub 仓库。
借助模拟输入,应用可以像用户一样操作网页。无论是测试自己的 Web 界面,还是自动操作无法修改的 Web 应用,都可以采用这种方式。
发送中。。。
您的个人 DotNetBrowser 试用密钥和快速入门指南将在几分钟内发送至您的电子邮箱。
