為什么選擇CEF
眾所周知桌面程序開發(fā)主流能數(shù)出來的就是MFC,Delphi忿危,JavaSE,WinForm没龙,WPF铺厨,QT等,但是他們都有較高的技術(shù)門檻硬纤,一般人要上手做出高效漂亮的界面來很難解滓。特別是當(dāng)需要做復(fù)雜的配置界面與控制面板類界面有一種來自內(nèi)心的無力,畢竟這些技術(shù)的職業(yè)前端都已經(jīng)很稀少了筝家。
幸好CEF的出現(xiàn)幫助我們解決了這個問題洼裤,選它有幾個優(yōu)點:
- 集成快,cef最簡單的就是一個瀏覽器溪王,使用最基礎(chǔ)的靜態(tài)界面+js+界面的Handler就可完成你想要的功能
- 開發(fā)快腮鞍,web前端技術(shù)是門檻最低、庫最多的界面技術(shù)了
- 利于開發(fā)各種復(fù)雜界面莹菱,對于美工的設(shè)計與個人的想法都能很好很簡單的實現(xiàn)
- 引入了web眾多優(yōu)秀特性
當(dāng)然它也存在缺點:
- 需要將cef打包進(jìn)程序移国,近百來兆
- 熟練的使用需要些時間研究,而官網(wǎng)的文檔很簡陋道伟,網(wǎng)上的教程很基礎(chǔ)
- 對chromium的版本追得不是那么緊
- 初次加載與渲染慢
- 遺傳了web的特點迹缀,沒有高性能
C#使用CEF——CEFSharp
其他技術(shù)接觸過但是了解不多,所以無法論述,本文是對C#下使用cef封裝庫CEFSharp使用的經(jīng)驗祝懂。cef主要使用方式就是將其自帶的瀏覽器添加到程序界面里面票摇,瀏覽器只做界面呈現(xiàn),控制全部通過后臺代碼實現(xiàn)嫂易。
本文適用51版本兄朋,尚未驗證其他版本是否適用,請謹(jǐn)遵參考
使用Winform作為載體
在選擇Winform前也是走了彎路怜械,因為覺得WPF更加簡潔和高效颅和,所以選擇了WPF來作為cef瀏覽器的載體。這樣界面配合起來更好用缕允,但是實踐后發(fā)現(xiàn)一個殘忍的事實:
最新的51版本對WPF兼容不太好峡扩, 運行時界面渲染會出現(xiàn)閃爍。
另外網(wǎng)上的資料和官方的文檔大都以Winfrom作為基礎(chǔ)講解障本,在學(xué)習(xí)上也更具有優(yōu)勢教届。
資源訪問方式
網(wǎng)絡(luò)資源
直接使用瀏覽器加載初始指定頁面即可,界面點擊會自動跳轉(zhuǎn)驾霜。網(wǎng)上資料泛濫案训。
本地資源
網(wǎng)上資料往往因為作者只是淺嘗則止和文章抄襲,形成了一個誤區(qū):
“CEF不能打包本地資源(html粪糙、js强霎、css、img)蓉冈,只能將其暴露在程序路徑下”城舞。
這是種觀點是錯誤的,使用基礎(chǔ)的方法通過相對路徑+注冊類給瀏覽器的方式確實不能將其打包寞酿。但是將資源添加到程序資源中家夺,通過SchemeHandler包裝請求與響應(yīng)的方式可以將二進(jìn)制資源以流的形式提供給cef瀏覽器訪問(具體方法后面有章節(jié)專門討論)》サ總的來說拉馋,將程序打包是可行的,了解技術(shù)使用Google不要被百度的一大抄的文章欺騙惨好。
因為cefsharp均支持winform與wpf椅邓,都進(jìn)行了試用。使用方式一致昧狮,唯一的區(qū)別就是51版本的wpf界面渲染時會出現(xiàn)閃動,所以后續(xù)cefsharp均是在winform下使用的板壮。
本地資源訪問方式
這種方式極不安全逗鸣,僅可用于學(xué)習(xí)研究,實際工程中不建議使用。
項目準(zhǔn)備
- 創(chuàng)建winform界面項目撒璧,指定目標(biāo)框架.net 4.5.2
- 使用nuget添加cefsharp.winform的引用(wpf添加cefsharp.wpf)
- 在編譯配置管理器中添加x64或者x86透葛,cefsharp不支持編譯成Any CPU
開始擼代碼
廢話不多說直接上代碼:
using System;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
namespace Ceftest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.Load += Form1_Load;
this.FormClosed += Form1_FormClosed;
}
private void Form1_Load(object sender, EventArgs e)
{
// 要先初始化才能用
Cef.Initialize();
// cefsharp提供的瀏覽器控件,一般用它充滿窗口就搞定了
ChromiumWebBrowser bs = new ChromiumWebBrowser("http://www.oschina.net")
{
// 填充整個父控件
Dock = DockStyle.Fill
};
// 添加到窗口的控件列表中
this.Controls.Add(bs);
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
// 結(jié)束時要銷毀
Cef.Shutdown();
}
}
}
在上面的代碼里可以看到瀏覽器初始化的時候訪問的是oschina卿樱,訪問web網(wǎng)頁很簡單給出地址即可僚害,頁面內(nèi)鏈接跳轉(zhuǎn)cef會自己處理的。如果需要操作加載指定頁面只需要調(diào)用瀏覽器控件的Load(string url)方法即可繁调。
訪問本地網(wǎng)頁
創(chuàng)建web前端界面helloworld.html
項目結(jié)構(gòu)如下圖:
添加生成事件
在項目屬性生成事件中添加后期生成事件如下萨蚕,就會在編譯生成后將web界面相關(guān)文件復(fù)制到生成目錄的Resource文件夾下。
xcopy /s /y $(ProjectDir)Resource $(TargetDir)Resource\
編輯hellworld.html
代碼:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title>hello world</title>
<link href="../Resource/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<h1 id="h1"></h1>
</body>
</html>
<script src="../Resource/js/jquery-1.10.2.min.js"></script>
<script src="../Resource/js/bootstrap.min.js"></script>
<script>
$(function(){
$("#h1").html("Hello world!");
});
</script>
這里要注意js與css文件的路徑蹄胰,需要在前面加上../岳遥,才能通過helloworld.html的相對路徑解析文件的絕對路徑,不然cef無法訪問資源文件裕寨。
修改cef瀏覽器控件初始化代碼
private void Form1_Load(object sender, EventArgs e)
{
// 要先初始化才能用
Cef.Initialize();
// 獲取生成路徑下網(wǎng)頁文件的絕對路徑
var fileName = Path.Combine(Directory.GetCurrentDirectory(), "Resource/helloworld.html");
// cefsharp提供的瀏覽器控件浩蓉,一般用它充滿窗口就搞定了
ChromiumWebBrowser bs = new ChromiumWebBrowser(fileName)
{
// 填充整個父控件
Dock = DockStyle.Fill
};
// 添加到窗口的控件列表中
this.Controls.Add(bs);
}
在上一步已經(jīng)提到了,cefsharp訪問本地的文件是以絕對路徑的方式進(jìn)行的宾袜,所以要先構(gòu)造初始界面的絕對路徑捻艳。如果需要加載其他界面,調(diào)用Load方法的時候也要傳遞網(wǎng)頁的絕對路徑庆猫。
F5完成认轨,彈出窗口,本地界面被顯示出來了
總結(jié)
這種直接訪問本地資源的方式簡單粗暴阅悍,適合學(xué)習(xí)好渠、調(diào)試使用。正常項目切記不能使用這種方式节视,因為它使得我們的界面代碼文件全部暴露在程序路徑下面拳锚,可以被隨意篡改。
當(dāng)然cefsharp也是早就想到了這個問題寻行,提供了一種將web文件打包到程序資源中提供訪問的方法霍掺。下面是Cefsharp本地開發(fā)時如何訪問程序資源,C#項目自帶有可以打包的資源拌蜘,而Cefsharp可以通過一些方式在瀏覽器上訪問到這些資源杆烁,進(jìn)而解決html文件的編譯打包問題。
程序資源訪問方式
建立項目添加web文件這些就不贅述简卧,參考上述博文即可兔魂。
準(zhǔn)備
一個新的Winform項目,文件結(jié)構(gòu)如下:
添加資源
打開Properties下的Resources.resx举娩,將Resource下所有用到的文件拖動到Resources.resx窗口中析校。
拿bootstrap.min.js舉例:
- 拖動過去构罗,界面如下
- 重命名資源(加上后綴名),因為默認(rèn)vs會幫你“智能”的去掉后綴名智玻,然而Resources.resx內(nèi)不允許建文件夾遂唧,所以在使用資源的時候并不能分得清。
- 調(diào)用吊奢,使用靜態(tài)資源類Resources 調(diào)用對應(yīng)的資源名稱即可獲得資源的值盖彭。
Resources.bootstrap_min_js;
需要注意的是:
資源分為很多種類型,常見的鍵值對页滚、文本文件類型為string召边,二進(jìn)制文件則為byte[],圖片為Bitmap類型逻谦。在使用的時候需要注意掌实。
照著這樣將所有的文件都添加進(jìn)去,這樣編譯后就些文件會當(dāng)成資源被打包到.exe中邦马。
注意:資源中存放的是文件在項目中的相對路徑贱鼻,所以移動后一定要重新新增資源,不然編譯就會找不到文件滋将。
使用資源
cefsharp能夠在瀏覽器中訪問到上述的資源是因為它內(nèi)部提供了一個Scheme機(jī)制邻悬,能夠截留自定義的Scheme請求,進(jìn)行處理和響應(yīng)随闽。這樣我們就可以將資源當(dāng)做請求的返回值父丰,提供給瀏覽器了。
正常的Scheme掘宪,如http蛾扇,https這些是不會被這個機(jī)制截留的,仍然會按照瀏覽器正常機(jī)制進(jìn)行魏滚。
創(chuàng)建自定義的Scheme類
cefsharp有一個自定義接口IResourceHandler镀首,實現(xiàn)這個接口就可以了。
PS:以前是叫ISchemeHandler的鼠次,后面改成IResourceHandler了更哄,我覺著這個還是蠻貼切的,資源處理器腥寇。另一層原因可能是因為把將接口這些虛擬自己納入進(jìn)來的原因吧成翩,是的,這個里面我們可以提供接口處理赦役。
自定義的Scheme類CustomSchemeHandler:
using CefSharp;
using Ceftest.Properties;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Ceftest
{
public class CustomSchemeHandler : IResourceHandler
{
private readonly IDictionary<string, byte[]> resources;
private string mimeType;
private MemoryStream stream;
public CustomSchemeHandler()
{
resources = new Dictionary<string, byte[]>
{
{"/asset/bootstrap_min.css", Encoding.UTF8.GetBytes(Resources.bootstrap_min_css) },
{"/asset/bootstrap_min.js", Encoding.UTF8.GetBytes(Resources.bootstrap_min_js) },
{"/asset/jquery.js", Encoding.UTF8.GetBytes(Resources.jquery_1_10_2_min_js) },
{"/helloworld.html", Encoding.UTF8.GetBytes(Resources.helloworld_html) },
};
}
// 當(dāng)收到請求時的處理器
bool IResourceHandler.ProcessRequest(IRequest request, ICallback callback)
{
var uri = new Uri(request.Url);
var resourceName = uri.AbsolutePath;
if (string.Equals(resourceName, "/exampleUri", StringComparison.OrdinalIgnoreCase))
{
// 這樣寫就可以針對特定的請求進(jìn)行處理和響應(yīng)
}
// 普通的文件資源處理
byte[] resource;
if (resources.TryGetValue(resourceName, out resource))
{
Task.Run(() =>
{
using (callback)
{
stream = new MemoryStream(resource);
var fileExtension = Path.GetExtension(resourceName);
mimeType = ResourceHandler.GetMimeType(fileExtension);
callback.Continue();
}
});
return true;
}
else
{
mimeType = ResourceHandler.GetMimeType(".html");
stream = null;
callback.Continue();
}
return false;
}
// 當(dāng)包裝responseheader的處理代碼
void IResourceHandler.GetResponseHeaders(IResponse response, out long responseLength, out string redirectUrl)
{
redirectUrl = null;
response.MimeType = mimeType;
if (stream != null)
{
responseLength = stream.Length;
response.StatusCode = (int)System.Net.HttpStatusCode.OK;
response.StatusText = "OK";
}
else
{
responseLength = 0;
response.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
response.StatusText = "NotFound";
}
}
// 當(dāng)包裝responsebody的處理代碼
bool IResourceHandler.ReadResponse(Stream dataOut, out int bytesRead, ICallback callback)
{
callback.Dispose();
if (stream == null)
{
bytesRead = 0;
return false;
}
var buffer = new byte[dataOut.Length];
bytesRead = stream.Read(buffer, 0, buffer.Length);
dataOut.Write(buffer, 0, buffer.Length);
return bytesRead > 0;
}
bool IResourceHandler.CanGetCookie(Cookie cookie)
{
return true;
}
bool IResourceHandler.CanSetCookie(Cookie cookie)
{
return true;
}
void IResourceHandler.Cancel()
{
}
void IDisposable.Dispose()
{
if (stream != null)
{
stream.Close();
stream.Dispose();
}
}
}
}
按照代碼實現(xiàn)的接口就可以將指定的收到的請求的資源返回麻敌,基本結(jié)構(gòu)上與cefsharp.example中的代碼差不多。但是這里有幾個需要說明:
- 資源字典沒有像官方demo使用<string掂摔,string>的鍵值對庸论,而是使用<string职辅,byte[]>這樣的,因為我們的資源都是通過流傳遞的二進(jìn)制回去聂示,所以byte[]更能存放圖片,文本簇秒,二進(jìn)制文件等多種類型鱼喉。
- 資源字典中定義的資源名,在使用時需要與之同名趋观,而不是使用文件名扛禽。
創(chuàng)建CustomSchemeHandler的工廠類
因為cefsharp采用了工廠的設(shè)計模式,所以還需要實現(xiàn)一個對應(yīng)的工廠類 CustomSchemeHandlerFactory:
using CefSharp;
namespace Ceftest
{
public class CustomSchemeHandlerFactory : ISchemeHandlerFactory
{
public const string SchemeName = "custom";// 自定義Scheme的名稱
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
{
if (schemeName == SchemeName)
{
return new CustomSchemeHandler();
}
else
{
// 這里的404界面未添加到程序里
var mimeType = ResourceHandler.GetMimeType(".html");
return ResourceHandler.FromFilePath("404.html", mimeType);
}
}
}
}
這里需要注意的兩點:
- URL的結(jié)構(gòu)為[scheme]://[domain]:[port]/[path]皱坛,工廠類中只對scheme中做了判斷编曼,如果想要對domain與path進(jìn)行判斷則對request中的url進(jìn)行解析即可。簡單情況下請求時剩辟,path就是我們的資源名掐场,domain隨便填。
- create方法就是在一個請求到來是調(diào)用贩猎,意味著每個請求都會new一個CustomSchemeHandler類進(jìn)行處理熊户。
在heloworld.html中添加資源引用
直接使用資源的名稱即可,不用帶上'/'吭服。代碼如下:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<link href="asset/bootstrap_min.css" rel="stylesheet" />
</head>
<body>
<h1 id="h1"></h1>
</body>
</html>
<script src="asset/bootstrap_min.js"></script>
<script src="asset/jquery.js"></script>
<script>
$(function(){
$("#h1").html("Hello world!");
});
</script>
最后一步
這一切準(zhǔn)備工作都做好了嚷堡,只需要將我們的自定義Scheme工廠注冊給cef,緊接著在瀏覽器中訪問我們的helloworld.html即可艇棕。form1代碼如下:
using System;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using System.IO;
namespace Ceftest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.Load += Form1_Load;
this.FormClosed += Form1_FormClosed;
}
private void Form1_Load(object sender, EventArgs e)
{
// 構(gòu)造一個初始化的配置類
var settings = new CefSettings();
// 在配置類中注冊自定義的schemeName與其對應(yīng)的工廠類
settings.RegisterScheme(new CefCustomScheme
{
SchemeName = CustomSchemeHandlerFactory.SchemeName,
SchemeHandlerFactory = new CustomSchemeHandlerFactory(),
});
// 要先初始化才能用
Cef.Initialize(settings);
// cefsharp提供的瀏覽器控件蝌戒,一般用它充滿窗口就搞定了
ChromiumWebBrowser bs = new ChromiumWebBrowser("custom://suibiantian/helloworld.html")
{
// 填充整個父控件
Dock = DockStyle.Fill
};
// 添加到窗口的控件列表中
this.Controls.Add(bs);
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
// 結(jié)束時要銷毀
Cef.Shutdown();
}
}
}
運行后即可看到helloworld的界面,去生成路徑下看看沒有網(wǎng)頁文件是吧沼琉。