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






4.95/5 (114投票s)
在 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 引擎)。 |
|
Awesomium HTML UI 引擎 |
它看起来很棒,并且拥有自己的 API。但我排除了它,因为它看起来是 Chromium 之上的一个 API。在这篇文章中,我决定研究 Chromium 和 CEF 框架。 |
|
GeckoFX |
GeckoFX 使用 Firefox 引擎。它看起来也很棒。与 Chromium 一样,它是一个开源的、完整的现代浏览器实现。 |
我们应该这样做吗?
这是我想要进行的编码之旅,看看使用 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”。这些文章将帮助你快速上手。
- 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
你可以轻松地通过 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 的常见问题解答解释说,此方法仅返回简单数据类型(int
、bool
、string
)。让我们设想我们要执行代码片段 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()
函数只返回简单数据类型(int
、bool
、string
)。因此,如果你需要返回一个复杂对象,你需要先将其转换为 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 嵌入式框架的维基百科条目 |
|
在 WPF 和 CefSharp 中显示 HTML,作者:Dirkster99 和 Alex Maitland |
https://codeproject.org.cn/Articles/881315/Display-HTML-in-WPF-and-CefSharp-Tutorial-Part |
Stack Overflow 上关于用更好的浏览器替换 .NET WebBrowser 控件的精彩讨论。其中讨论了许多替代方案。 |
|
CefSharp 常见问题解答 |
https://github.com/cefsharp/CefSharp/wiki/Frequently-asked-questions |
.NET 包装器,用于替代 Awesomium、Web-Browser 框架 |
https://github.com/khrona/AwesomiumSharp |
WebGL 演示 |
http://www.bongiovi.tw/experiments/webgl/blossom/ |
Canvas 气泡 |
http://blog.hostgrenade.com/2012/04/25/html5-canvas-bubble-demo-v2/ |
Bootstrap 表单验证示例。 |
|
本文档中的动画 GIF 是使用 GifCam 创建的 |
Chromium 依赖项
在将 Chromium 集成到你的项目中后,你会在项目文件夹中找到更多 DLL。此页面:https://github.com/cefsharp/cef-binary/blob/master/README.txt 描述了这些文件,我已将其包含在此处以求完整。
描述 |
文件 |
注释 |
CEF 核心库 |
libcef.dll |
|
Unicode 支持 |
icudtl.dat |
|
本地化资源 |
locales/ 目录 |
包含 WebKit UI 控件的本地化字符串。 只需分发已配置的语言环境。如果没有配置语言环境,将使用默认语言环境“ 可以使用 |
其他资源 |
cef.pak |
包含 WebKit 图像和检查器资源。Pack 文件加载可以
|
FFmpeg 音频和视频支持 |
ffmpegsumo.dll |
没有此组件,HTML5 音频和视频将无法工作。 |
PDF 支持 |
pdf.dll |
没有此组件,打印将无法工作。 |
Angle 和 Direct3D 支持 |
d3dcompiler_43.dll(Windows XP 所需) |
没有这些组件,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 调用 - 它将在应用程序退出时自动调用