65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WinForms 应用程序中使用 HTML 作为 UI 元素,借助 Chrome / Chromium 嵌入式框架 (CEF)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (114投票s)

2015 年 5 月 11 日

CPOL

14分钟阅读

viewsIcon

425269

downloadIcon

11

在 WinForms 应用程序中使用 HTML 作为 UI 元素,借助 Chrome / Chromium 嵌入式框架 (CEF)

引言

众所周知,所有激动人心的功能都发生在 Web 上!—— 似乎每隔几周就会诞生一个新的 JavaScript/HTML5 框架。有了 HTML5,浏览器本身就包含了一些很棒的工具。如今,仅凭 CSS 就能实现令人惊叹的效果。再加上 Canvas 支持和 WebGL,浏览器就是一切!

我们能否在 WinForms 应用程序中利用并包含其中的一些功能,岂不美哉?

我注意到越来越多的传统 Windows 桌面应用程序的 UI 具有 HTML 的“感觉”。例如,下图的 TeamViewer 应用程序。诚然,我不是说它是 C#/WinForms 应用程序,但其用户界面大量借鉴了 HTML。

传统上,WinForms 当然一直自带 WebBrowser 控件,但通常这种控件都比较糟糕,因为它不提供最新功能,而且你必须依赖客户端计算机上安装的 Internet Explorer 版本。

此项目的源代码可以在 GitHub 上找到,地址为:

目标

我这个项目的目标是创建一个简单的概念验证 C# WinForms 应用程序,该应用程序可以利用 HTML 作为用户界面。在此基础上,我们应该能够实现以下功能:

  • 在 WinForms 应用程序中显示 HTML
  • 从 C# 调用 JavaScript 函数
  • 从 JavaScript 调用 C# 函数
  • 在 C# 和 JavaScript 之间双向传递数据
  • 使用 Chrome 开发者工具调试 HTML/JavaScript

Chromium 和 Chromium 嵌入式框架

为了实现上述目标,我将使用开源的 Chromium 网页浏览器,特别是 Chromium 嵌入式框架 (CEF)。在其网站上,Chromium 嵌入式框架 (CEF) 是一个用于将基于 Chromium 的浏览器嵌入到其他应用程序中的简单框架。

特别鸣谢

在研究将 HTML 内容嵌入 .NET 应用程序时,我偶然发现了一些其他项目,它们值得一提。

名称

注释

在哪里找到它

内置 C# Web 浏览器控件

该控件实际上可以与 JavaScript 进行调用,反之亦然。该控件的问题在于,你被绑定到宿主操作系统上安装的 Internet Explorer 版本,并且没有运行时调试功能。

https://msdn.microsoft.com/en-us/library/system.windows.forms.webbrowser%28v=vs.110%29.aspx

HTML Renderer

这是一个很棒的小框架。它轻量级,仅包含 2 个 .DLL 文件。但是,我无法确定是否可以调用 JavaScript 或 JavaScript 是否可以调用回 C# 代码(我认为它不包含 JS 引擎)。

https://htmlrenderer.codeplex.com/

Awesomium HTML UI 引擎

它看起来很棒,并且拥有自己的 API。但我排除了它,因为它看起来是 Chromium 之上的一个 API。在这篇文章中,我决定研究 Chromium 和 CEF 框架。

http://www.awesomium.com/

GeckoFX

GeckoFX 使用 Firefox 引擎。它看起来也很棒。与 Chromium 一样,它是一个开源的、完整的现代浏览器实现。

https://bitbucket.org/geckofx

我们应该这样做吗?

这是我想要进行的编码之旅,看看使用 Chromium 项目可以完成什么。《侏罗纪公园》中的那句台词:“我们太专注于是否能做到,而没停下来想是否应该这样做”在这里很适用。你需要自己决定什么对你和你的项目是正确的。对我来说,我认为这些东西很酷!

使用 Chromium / CEF 的优点

我选择 Chromium 而不是其他浏览器引擎的原因是,首先,它是一个完全现代化的浏览器,并且与 CEF 结合使用时,可以让你轻松地在 WinForms 应用程序中实现基于 HTML5 的用户界面。Chromium 本身支持我们期望现代 HTML5 浏览器所拥有的一切最新功能。

  • HTML5
  • CSS3
  • 画布
  • SVG
  • WebGL
  • 开发者工具

要了解 Chromium 在 HTML5 和 CSS3 功能方面的完整性,我们可以访问

但选择 Chromium 的主要原因是,JavaScript 和 C# 代码之间的交互非常容易。我们可以轻松地从 JavaScript 调用 C# 代码,反之亦然。

最后,最棒的功能是 Chromium 包含 Chrome 附带的完整调试开发者工具。我们将要在应用程序中编写 JavaScript 代码或包含 JavaScript 库,并且能够直接在应用程序中调试它们至关重要。这使得 Chromium 脱颖而出。

使用 Chromium 的缺点

好的,那么使用 Chromium 的缺点是什么呢?Chromium 绝不轻量级。它依赖于许多 DLL 文件,你需要将它们与你的应用程序一起打包。但对我来说,为了包含如此强大的功能,这只是一个小小的代价。

在 WinForms 项目中设置 Chromium

要将 Chromium 集成到你的 WinForms 项目中,请参阅 Dirkster99 和 Alex Maitland 的系列文章“在 WPF 和 CefSharp 中显示 HTML”。这些文章将帮助你快速上手。

你可以轻松地通过 Nuget 将 Cefsharp 安装到你的解决方案中。

使用 Nuget 将 CefSharp 安装到你的 WinForms 项目中的示例。

安装 CefSharp 后,你会注意到这个警告:

同样,参考上面 Dirkster99 和 Alex Maitland 的文章,我们需要指定我们正在定位的平台。我将选择 x86。

设置平台生成 x86 和 x64 的示例。

设置好这些之后,项目就可以成功生成了。

此时,我们的项目包含了使用 Chromium 所需的所有功能。现在,让我们看看如何使用它。

Chromium API 快速导览

添加 Chromium 网页浏览器控件

要将 Chromium 网页浏览器添加到 Form 中,只需将以下代码添加到 Forms 的 Load()FormClosing() 方法中即可。

代码片段 1:Chromium 入门
private void Form1_Load(object sender, EventArgs e)
{
    Cef.Initialize();
    ChromiumWebBrowser myBrowser = new ChromiumWebBrowser("http://www.maps.google.com" );
    this.Controls.Add(myBrowser);
}

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    Cef.Shutdown();
}

注意: Cef.Initialize();Cef.Shutdown(); 函数调用只需在应用程序中调用一次。

这将把 Chromium 浏览器控件添加到主窗体。在上面的代码中,我们将默认 URL 设置为 http://www.maps.google.com。这就是我们最终的窗体外观:

示例应用程序截图。

现在,让我们探索一下我们可以用它做什么。

显示 Chromium 开发者工具

显示页面的开发者工具只需一个简单的函数调用即可。

代码片段 2:显示 Chromium 开发者工具
private void buttonShowDevTools_Click(object sender, EventArgs e)
{
    m_chromeBrowser.ShowDevTools();
}

这将弹出熟悉的 Chrome 开发者工具。你可以访问所有功能,并允许你检查 DOM 和元素、调试 JavaScript 代码、查看 CSS 样式、通过控制台运行命令等。

Chrome 开发者工具截图

Web 开发者习惯于查看开发者工具来检查/修改代码,通常通过按 F12 键。我在左侧系统菜单(见图 3)中添加了一个菜单选项,允许你在运行时查看 Chrome 开发者工具。为了插入菜单选项,我创建了一个实用类来注入该菜单选项。

[作为旁注,我最初尝试捕获窗体上的 F12 键,但是即使将 KeyPreview 设置为 true,Chrome 似乎也捕获了所有按键,并且 WinForm 类从未捕获到键预览。]

Chrome 开发者工具菜单选项截图

要启用此菜单,请在窗体的 Load 方法中调用以下 static 类:

ChromeDevToolsSystemMenu.CreateSysMenu(this);

然后重写 WndProc 方法,监听 SYSMENU_CHROME_DEV_TOOLS 菜单选择。

代码片段 3:加载自定义 HTML 的示例
protected override void WndProc(ref Message m)
{
    base.WndProc(ref m);

    // Test if the About item was selected from the system menu
    if ((m.Msg == ChromeDevToolsSystemMenu.WM_SYSCOMMAND) && 
       ((int )m.WParam == ChromeDevToolsSystemMenu.SYSMENU_CHROME_DEV_TOOLS))
    {
        m_chromeBrowser.ShowDevTools();
    }
}

ChromeDevToolsSystemMenu 类的代码包含在示例项目中。此处不详细描述,因为它偏离了文章的主要主题。

查找你正在使用的 Chromium 版本

要查找你在应用程序中使用的 Chrome 版本,只需导航到以下 URL:

chrome://version/

这将告诉你你正在运行的 Chromium 版本以及 CEF 的版本。

Chrome 版本截图

在我这里,我正在运行 39 版本,该版本发布于 2014 年 8 月左右。

动态显示 Chromium 中的 HTML

将 Chromium 集成到 WinForms 应用中的主要目的是显示自定义 HTML。要做到这一点,你需要调用 LoadHtml()。该函数需要:

代码片段 4:加载自定义 HTML 的示例
private void buttonCustomHTML_Click(object sender, EventArgs e)
{
    m_chromeBrowser.LoadHtml( "Hello world" , "http://customrendering/" );
}

在 Visual Studio 项目中包含 HTML 资源

我将按照以下目录结构组织 HTML 资源/资产。你可以创建适合你的目录结构。

目录结构截图

你可以直接在 Visual Studio 的解决方案资源管理器中创建这些目录,但我发现直接在 Windows 资源管理器中创建目录结构更容易。

然后,在 Visual Studio 中,单击解决方案资源管理器中的“显示所有文件”图标(如下所示)。之后,已添加的目录将显示在解决方案资源管理器中。然后你需要选择该目录,右键单击,然后选择“包含到项目”.

解决方案资源管理器中“显示所有文件”按钮的截图。

右键单击上下文菜单中“包含到项目”的截图。

最后,在 Visual Studio 中,确保“生成操作”属性设置为“内容”,并且“复制到输出目录”属性设置为“始终复制”。

设置生成操作的截图。

这将确保在生成应用程序时,将这些文件包含在输出目录中。

此时,我们已经设置好了环境。现在,让我们看看如何与 JavaScript 和 C# 进行交互。

JavaScript 与 C# 之间的交互(反之亦然)

首先,让我们创建一个简单的数据对象(模型),我们可以用它在 C# 和 JS 之间传递。由于缺乏原创性,我将创建一个 Person 对象,如下所示:

代码片段 5:C# 数据/模型类
public class Person
{
    public Person( string firstName, string lastName, DateTime birthDate)
    {
        FirstName = firstName;
        LastName = lastName;
        DateOfBirth = birthDate;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public int SkillLevel { get; set; }
}

接下来,让我们创建一个简单的业务逻辑类,JavaScript 可以调用其函数并与我们的 WinForms App 进行交互。我还使用了来自 http://www.newtonsoft.com/ 的 Json.NET 来序列化/反序列化任何数据到/从 JSON 格式。

JavaScriptInteractionObj 类没有任何特别之处。它仅仅是一系列可以在 JavaScript 中调用的函数。

代码片段 6:C# 业务层对象(相当于服务器端调用)
public class JavaScriptInteractionObj
{
    public Person m_theMan = null;

    public JavaScriptInteractionObj()
    {
        m_theMan = new Person( "Bat", "Man" , DateTime .Now);
    }

    public string SomeFunction()
    {
        return "yippieee";
    }

    public string GetPerson()
    {
        var p1 = new Person( "Bruce", "Banner" , DateTime .Now );

        string json = JsonConvert.SerializeObject(p1);
        return json;
    }

    public string ErrorFunction()
    {
        return null;
    }

    public string GetListOfPeople()
    {
        List< Person> peopleList = new List< Person>();

        peopleList.Add( new Person( "Scooby", "Doo" , DateTime .Now));
        peopleList.Add( new Person( "Buggs", "Bunny" , DateTime .Now));
        peopleList.Add( new Person( "Daffy", "Duck" , DateTime .Now));
        peopleList.Add( new Person( "Fred", "Flinstone" , DateTime .Now));
        peopleList.Add( new Person( "Iron", "Man" , DateTime .Now));

        string json = JsonConvert.SerializeObject(peopleList);
        return json;
    }
 }

接下来,我们需要将 JavaScriptInteractionObj 对象绑定到 Chromium 浏览器。我们使用 RegisterJsObject() 函数来实现此目的。

此函数接受要在 JavaScript 端访问的对象名称以及 C# 对象本身。

代码片段 7:将 C# 对象注册到 JavaScript 的示例。
private void buttonRegisterCSharpObject_Click(object sender, EventArgs e)
{
    m_chromeBrowser.RegisterJsObject( "winformObj", new JavaScriptInteractionObj());

    string page = string.Format("{0}HTMLEmbeddedResources/html/WinformInteractionExample.html" , 
                  EmbeddedResourceUtils.GetAppLocation());
    m_chromeBrowser.Load(page);
}

在上面的 C# 示例(代码片段 7)中,名称“winformObj”将从 JavaScript 中访问,使得以下 JavaScript 代码成为可能:

此时,我们将 winformObj 对象绑定到了 Chromium 浏览器的窗口。这意味着,以下代码可以调用到 C# 中的 GetListOfPeople()

代码片段 8:从 JavaScript 调用 C# 代码的示例。
function CallWinformFunc()
{
     var list = winformObj.getListOfPeople(); // Call C# Function
     for (var nLoopCnt = 0; nLoopCnt < list.length; nLoopCnt++) {
           var person = list[nLoopCnt];
     }
}<button onclick="CallWinformFunc()">Test Winform Interaction</button>

在上面的示例中,我们有一个 HTML 按钮,单击时会调用代码片段 6 中描述的 C# 函数 GetListOfPeople()

注意: 我注意到的一点是,RegisterJsObject() 函数应该在 Web 浏览器创建后立即调用。如果这样做,调用可能会失败,并且在 JavaScript 中,该对象将为 null。#

从 WinForms / C# 执行 JavaScript 代码

从 C# 端,你可以执行任何临时 JavaScript 代码或简单地通过调用 ExecScriptAsync() 来执行一个函数。例如,以下代码片段将在浏览器页面上直接执行脚本,将背景变为红色:

代码片段 9:从 C# 执行 JavaScript 代码的示例
private void buttonExecJavaScriptFromWinforms_Click(object sender, EventArgs e)
{
    var script = "document.body.style.backgroundColor = 'red';";

    m_chromeBrowser.ExecuteScriptAsync(script);
}

从 JavaScript 返回数据到 C# / WinForms

想象一下这样的场景:你需要从 C# / WinForms 端找出变量的值,或者一个函数调用的值(看看它是否成功)。使用上述方法,我们无法从

CefSharp 的常见问题解答解释说,此方法仅返回简单数据类型(intboolstring)。让我们设想我们要执行代码片段 10 中的以下临时代码,它返回一个 int

我们将从 C# 中调用的 JavaScript 函数的代码片段 10
function tempFunction() {
    var w = window.innerWidth;
    var h = window.innerHeight;

    return w*h;
}
tempFunction();

以下是我们如何使用 Chromium 执行该代码:

代码片段 11:执行返回值的 JavaScript 代码并从 C# 获取的示例
private void buttonReturnDataFromJavaScript_Click(object sender, EventArgs e)
{
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("function tempFunction() {");
    sb.AppendLine("     var w = window.innerWidth;");
    sb.AppendLine("     var h = window.innerHeight;");
    sb.AppendLine("");
    sb.AppendLine("     return w*h;");
    sb.AppendLine("}");
    sb.AppendLine("tempFunction();");

    var task = m_chromeBrowser.EvaluateScriptAsync(sb.ToString());

    task.ContinueWith(t =>
    {
        if (!t.IsFaulted)
        {
            var response = t.Result;

            if ( response.Success == true )
            {
                MessageBox.Show( response.Result.ToString() );
            }
        }
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

上面的示例演示了从 JavaScript 返回一个简单的 int 值。

但是,如果我们想从 JavaScript 返回一个复杂对象到 C# 呢?

好的,请记住 EvaluateScriptAsync() 函数只返回简单数据类型(intboolstring)。因此,如果你需要返回一个复杂对象,你需要先将其转换为 JSON,然后将对象作为 string 返回。

以下代码示例演示了如何将一个复杂对象(一个 person 对象)从 JavaScript 返回到 C#。

代码片段 12:执行返回值的 JavaScript 代码并从 C# 获取的示例
private void buttonReturnDataFromJavaScript2_Click(object sender, EventArgs e)
{
    // Step 01: create a simple html page (include jquery so we have access to json object
    StringBuilder htmlPage = new StringBuilder();
    htmlPage.AppendLine("");
    htmlPage.AppendLine("");
    htmlPage.AppendLine("");
    htmlPage.AppendLine("");
    htmlPage.AppendLine("Hello world 2");
    htmlPage.AppendLine("");
 
    // Step 02: Load the Page
    m_chromeBrowser.LoadHtml(htmlPage.ToString(), "http://customrendering/");
 
    // Step 03: Define and Execute some ad-hoc JS that returns an object back to C#
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("function tempFunction() {");
    sb.AppendLine("     // create a JS object");
    sb.AppendLine("     var person = {firstName:'John', lastName:'Maclaine', age:23, eyeColor:'blue'};");
    sb.AppendLine("");
    sb.AppendLine("     // Important: convert object to string before returning to C#");
    sb.AppendLine("     return JSON.stringify(person);");
    sb.AppendLine("}");
    sb.AppendLine("tempFunction();");
 
    var task = m_chromeBrowser.EvaluateScriptAsync(sb.ToString());
 
    task.ContinueWith(t =>
    {
        if (!t.IsFaulted)
        {
            // Step 04: Recieve value from JS
            var response = t.Result;
 
            if (response.Success == true)
            {
                // Use JSON.net to convert to object;
                MessageBox.Show(response.Result.ToString());
            }
        }
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

如上所示,在临时 JavaScript 代码中,我们声明了一个 person 对象,如下所示:

var person = {firstName:"John", lastName:"Maclaine", age:23, eyeColor:"blue"};

我们做的最后一件事是使用“JSON.stringify()”方法将对象转换为 string。一旦回到 C#,我们就可以使用 Newtonsoft JSON.NET 将 string 转换回对象。

JavaScript 中的小写函数调用

需要注意的一点是,从 JavaScript 调用 C# 方法时的大小写问题。如下所示,C# 类有一个名为 SomeFunction() 的方法,它返回一个 sting。注意函数名以大写的“S”开头。

如果你查看 Chrome 开发者工具窗口,你可以看到,当你尝试使用大写字母调用该函数时,会出现类型错误(未定义函数)。Chrome 会将函数名转换为小写。当你调用相同的函数但使用小写“s”时,它就能工作。

HTML5 演示功能

为了初步了解可能实现的功能,我在示例窗体中包含了一些演示。其中一些 HTML 窗体与 WinForms 交互。其他则仅仅展示了通过将 Chromium 集成到你的项目中可用的功能/可能性。

我在几个示例中包含了 Bootstrap,以演示如何从 HTML 接收用户输入。在另一个示例中,我使用了图表库 AmCharts 来显示一个简单的折线图。其他示例则展示了 Canvas 功能和 WebGL 功能。

结论

这是将 Chromium 网页浏览器集成到 WinForms 应用程序中以利用 HTML 作为用户界面的简要介绍。Chromium 网页浏览器是一个完整而全面的现代框架,用于嵌入此类功能。简而言之,Chromium 嵌入式框架是为在 WinForms 应用程序中嵌入基于 HTML5 的 GUI 的绝佳框架。

资源

Chromium 嵌入式框架的维基百科条目

http://en.wikipedia.org/wiki/Chromium_Embedded_Framework

在 WPF 和 CefSharp 中显示 HTML,作者:Dirkster99 和 Alex Maitland

https://codeproject.org.cn/Articles/881315/Display-HTML-in-WPF-and-CefSharp-Tutorial-Part
https://codeproject.org.cn/Articles/887148/Display-HTML-in-WPF-and-CefSharp-Tutorial-Part

Stack Overflow 上关于用更好的浏览器替换 .NET WebBrowser 控件的精彩讨论。其中讨论了许多替代方案。

http://stackoverflow.com/questions/790542/replacing-net-webbrowser-control-with-a-better-browser-like-chrome

CefSharp 常见问题解答

https://github.com/cefsharp/CefSharp/wiki/Frequently-asked-questions

.NET 包装器,用于替代 Awesomium、Web-Browser 框架

https://github.com/khrona/AwesomiumSharp
http://docs.awesomium.net/
http://wiki.awesomium.net/

WebGL 演示

http://www.bongiovi.tw/experiments/webgl/blossom/

Canvas 气泡

http://blog.hostgrenade.com/2012/04/25/html5-canvas-bubble-demo-v2/

Bootstrap 表单验证示例。

http://formvalidation.io

本文档中的动画 GIF 是使用 GifCam 创建的

http://blog.bahraniapps.com/gifcam/

Chromium 依赖项

在将 Chromium 集成到你的项目中后,你会在项目文件夹中找到更多 DLL。此页面:https://github.com/cefsharp/cef-binary/blob/master/README.txt 描述了这些文件,我已将其包含在此处以求完整。

描述

文件

注释

CEF 核心库

libcef.dll

 

Unicode 支持

icudtl.dat

 

本地化资源

locales/ 目录

包含 WebKit UI 控件的本地化字符串。
一个 .pak 文件将根据 CefSettings.locale 值从该文件夹加载。

只需分发已配置的语言环境。如果没有配置语言环境,将使用默认语言环境“en-US”。

可以使用 CefSettings.pack_loading_disabled 完全禁用语言环境文件加载。可以使用 CefSettings.locales_dir_path 自定义语言环境文件夹路径。

其他资源

cef.pak
cef_100_percent.pak
cef_200_percent.pak devtools_resources.pak

包含 WebKit 图像和检查器资源。Pack 文件加载可以

  • 使用 CefSettings.pack_loading_disabled 完全禁用。
  • 可以使用 CefSettings.resources_dir_path 自定义资源目录路径。

FFmpeg 音频和视频支持

ffmpegsumo.dll

没有此组件,HTML5 音频和视频将无法工作。

PDF 支持

pdf.dll

没有此组件,打印将无法工作。

Angle 和 Direct3D 支持

d3dcompiler_43.dll(Windows XP 所需)
d3dcompiler_47.dll(Windows Vista 及更新版本所需)
libEGL.dll
libGLESv2.dll

没有这些组件,HTML5 加速内容,如 2D Canvas、3D CSS 和 WebGL 将无法工作。

Windows Vista 64 位沙盒支持(仅限 32 位发行版)

wow_helper.exe

没有此组件,CEF 的 32 位版本将在启用沙盒的 64 位 Vista 计算机上运行。

勘误

如果你/何时遇到任何错误,或对更好的做法有任何想法,请随时评论并反馈。非常欢迎提出意见!

历史

24-02-206 - Github 项目更新及修复,由 Alex Maitland 提供

- 升级到 CefSharp 47.0.2
- 删除程序包和 bin 文件夹
- 从解决方案中删除 AnyCpu 目标 
- ShowDevTools 现在是一个扩展方法,来自 CefSharp 命名空间
- 从项目中删除空文件夹
- 修复 bootstrap 示例 URL - 路径不正确
- Cef.Initialize 现在默认由 ChromiumWebBrowser 调用 
- 删除 Cef.Shutdown 调用 - 它将在应用程序退出时自动调用

© . All rights reserved.