今天跟大家分享CefSharp,它是我之前在做医院自助机上的软件所用到的技术,利用Chrome内核、做到桌面端可以前后端分离,UI部分给到前端去写,现在大多数医院自助机也采用类似架构。
优点
- web负责壳子,现代化开发,让专业前端构建精美UI,不同客户(医院)换版只用换UI层,大大提升开发效率;
- .NET负责里子,底层用C#可以通过串口、USB、Socket等直接与身份证读卡器、银行卡读卡器、凭条打印机等设备通信。
下面用一个最小WPF Demo把C#、JS互调讲通,如图

页面点「取本机名 + 时间」,底下 WPF 状态栏会变;再在底下输入框点「调用 JS」,页面右侧和日志会出现 C# 推过来的字。两边通了,后面接 Vue、本地报表、设备页都是同一套逻辑。
创建项目
CefSharp必须 x64(或 x86)。我这边用.NET 8 + CefSharp.Wpf.NETCore 151:
dotnet new wpf -n CefSharpWpfDemo -f net8.0
cd CefSharpWpfDemo
dotnet add package CefSharp.Wpf.NETCore --version 151.3.240
csproj 里把平台固定,并把html拷到输出目录:
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<PlatformTarget>x64</PlatformTarget>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CefSharp.Wpf.NETCore" Version="151.3.240" />
</ItemGroup>
<ItemGroup>
<Content Include="wwwroot\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
机器上必须装 VC++ 2022 x64 运行库,缺了会报找不到 libcef.dll 一类的错。
初始化Chromium
Cef.Initialize 必须在创建 ChromiumWebBrowser 之前。StartupUri 打开主窗口前会先跑 App 构造函数,放这里正好。新版本 CEF 必须设 CachePath,不设初始化会失败。
App.xaml.cs:
using System.IO;
using System.Windows;
using CefSharp;
using CefSharp.Wpf;
namespace CefSharpWpfDemo;
public partial class App : Application
{
public App()
{
var settings = new CefSettings
{
CachePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"CefSharpWpfDemo", "Cache"),
LogSeverity = LogSeverity.Disable
};
Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null);
}
private void OnExit(object sender, ExitEventArgs e)
{
Cef.Shutdown();
}
}
App.xaml 记得加上退出:
<Application x:Class="CefSharpWpfDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
Exit="OnExit">
<Application.Resources />
</Application>
主窗口中间放浏览器,底下留一排C#控件:
<Window x:Class="CefSharpWpfDemo.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="CefSharp 互调 Demo"
Height="720" Width="980"
WindowStartupLocation="CenterScreen">
<DockPanel>
<cef:ChromiumWebBrowser x:Name="Browser"/>
</DockPanel>
</Window>
页面不要走file://,容易被安全策略拦住。用 LoadHtml给个假地址就行:
var htmlPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "index.html");
Browser.LoadHtml(File.ReadAllText(htmlPath), "https://cefsharp.demo/");
真实部署记得用真实地址
JS调用C#
分别是:C#建对象 → 注册成 bridge → 页面里 BindObjectAsync 之后当Promise调。
暴露给页面的类:
public class JsBridge
{
private readonly MainWindow _window;
public JsBridge(MainWindow window) => _window = window;
public string GetHostInfo()
{
return $"{Environment.MachineName} | {DateTime.Now:HH:mm:ss}";
}
public string SendToCsharp(string message)
{
var ack = "";
_window.Dispatcher.Invoke(() =>
{
_window.OnMessageFromJs(message);
ack = "C# 已收到";
});
return ack;
}
}
注意两点:
- 方法名会被转成小驼峰。C# 写
GetHostInfo,JS 里是bridge.getHostInfo()。 - 改 WPF 控件必须回 UI 线程,CefSharp 回调不在 Dispatcher 上,直接改
TextBlock会炸。
注册放在 InitializeComponent 后面:
Browser.JavascriptObjectRepository.Settings.JavascriptBindingApiEnabled = true;
Browser.JavascriptObjectRepository.Register(
"bridge",
new JsBridge(this),
BindingOptions.DefaultBinder);
页面侧先绑定,再调。返回值是 Promise,记得 await:
async function ensureBridge() {
if (!window.bridge) {
await CefSharp.BindObjectAsync("bridge");
}
return window.bridge;
}
async function getHostInfo() {
const bridge = await ensureBridge();
const info = await bridge.getHostInfo();
log("getHostInfo() = " + info);
}
async function callCsharp() {
const msg = document.getElementById("jsText").value;
const bridge = await ensureBridge();
const ack = await bridge.sendToCsharp(msg);
log("发送「" + msg + "」,返回 " + ack);
}
CefSharp 这个对象是内核注入的,普通浏览器打开html没有它。必须嵌在 ChromiumWebBrowser 里才存在。
C#调用JS
反过来就是EvaluateScriptAsync往页面写段脚本。字符串一定要序列化,切记别自己拼引号,有换行、引号到时分不清。
页面先挂好回调:
window.onCsharpMessage = function (text) {
document.getElementById("fromCs").textContent = text;
log('C# → JS onCsharpMessage("' + text + '")');
};
WPF 按钮:
private async void BtnSendToJs_Click(object sender, RoutedEventArgs e)
{
var payload = JsonSerializer.Serialize(TxtToJs.Text);
await Browser.EvaluateScriptAsync($"window.onCsharpMessage({payload})");
}
只想执行、不要返回值用 ExecuteScriptAsync;要拿结果,用 EvaluateScriptAsync:
var response = await Browser.EvaluateScriptAsync("document.title");
if (response.Success)
{
TxtFromJs.Text = $"C# ← 页面标题:{response.Result}";
}
三种调用方式:
- JS → C#:
BindObjectAsync+bridge.xxx(),页面要本机文件、硬件、已登录用户 - C# → JS:
EvaluateScriptAsync("fn(arg)"),把结果、进度推给页面 - C#读页面:
EvaluateScriptAsync("document.title"),拿标题、表单值、当前路由