今天跟大家分享CefSharp,它是我之前在做医院自助机上的软件所用到的技术,利用Chrome内核、做到桌面端可以前后端分离,UI部分给到前端去写,现在大多数医院自助机也采用类似架构。

仓库: https://github.com/cefsharp/cefsharp

优点

  • 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"),拿标题、表单值、当前路由