Windows桌面靜態(tài)界面技術(shù)選型&實踐——CEF(CefSharp)

為什么選擇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)如下圖:

vs項目結(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完成认轨,彈出窗口,本地界面被顯示出來了

Hello world

總結(jié)

這種直接訪問本地資源的方式簡單粗暴阅悍,適合學(xué)習(xí)好渠、調(diào)試使用。正常項目切記不能使用這種方式节视,因為它使得我們的界面代碼文件全部暴露在程序路徑下面拳锚,可以被隨意篡改。

當(dāng)然cefsharp也是早就想到了這個問題寻行,提供了一種將web文件打包到程序資源中提供訪問的方法霍掺。下面是Cefsharp本地開發(fā)時如何訪問程序資源,C#項目自帶有可以打包的資源拌蜘,而Cefsharp可以通過一些方式在瀏覽器上訪問到這些資源杆烁,進(jìn)而解決html文件的編譯打包問題。

程序資源訪問方式

建立項目添加web文件這些就不贅述简卧,參考上述博文即可兔魂。

準(zhǔn)備

一個新的Winform項目,文件結(jié)構(gòu)如下:

項目文件結(jié)構(gòu)

添加資源

打開Properties下的Resources.resx举娩,將Resource下所有用到的文件拖動到Resources.resx窗口中析校。
拿bootstrap.min.js舉例:

  1. 拖動過去构罗,界面如下
添加的資源文件
  1. 重命名資源(加上后綴名),因為默認(rèn)vs會幫你“智能”的去掉后綴名智玻,然而Resources.resx內(nèi)不允許建文件夾遂唧,所以在使用資源的時候并不能分得清。
重命名文件名
  1. 調(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中的代碼差不多。但是這里有幾個需要說明:

  1. 資源字典沒有像官方demo使用<string掂摔,string>的鍵值對庸论,而是使用<string职辅,byte[]>這樣的,因為我們的資源都是通過流傳遞的二進(jìn)制回去聂示,所以byte[]更能存放圖片,文本簇秒,二進(jìn)制文件等多種類型鱼喉。
  2. 資源字典中定義的資源名,在使用時需要與之同名趋观,而不是使用文件名扛禽。

創(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);
            }
        }
    }
}

這里需要注意的兩點:

  1. URL的結(jié)構(gòu)為[scheme]://[domain]:[port]/[path]皱坛,工廠類中只對scheme中做了判斷编曼,如果想要對domain與path進(jìn)行判斷則對request中的url進(jìn)行解析即可。簡單情況下請求時剩辟,path就是我們的資源名掐场,domain隨便填。
  2. 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)頁文件是吧沼琉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末北苟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子刺桃,更是在濱河造成了極大的恐慌粹淋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑟慈,死亡現(xiàn)場離奇詭異桃移,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)葛碧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門借杰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人进泼,你說我怎么就攤上這事蔗衡∠怂洌” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵绞惦,是天一觀的道長逼纸。 經(jīng)常有香客問我,道長济蝉,這世上最難降的妖魔是什么杰刽? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮王滤,結(jié)果婚禮上贺嫂,老公的妹妹穿的比我還像新娘。我一直安慰自己雁乡,他們只是感情好第喳,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著踱稍,像睡著了一般曲饱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寞射,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天渔工,我揣著相機(jī)與錄音,去河邊找鬼桥温。 笑死引矩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侵浸。 我是一名探鬼主播旺韭,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼掏觉!你這毒婦竟也來了区端?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤澳腹,失蹤者是張志新(化名)和其女友劉穎织盼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酱塔,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡沥邻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了羊娃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唐全。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蕊玷,靈堂內(nèi)的尸體忽然破棺而出邮利,到底是詐尸還是另有隱情弥雹,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布延届,位于F島的核電站剪勿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏方庭。R本人自食惡果不足惜窗宦,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望二鳄。 院中可真熱鬧,春花似錦媒怯、人聲如沸订讼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽欺殿。三九已至,卻和暖如春鳖敷,著一層夾襖步出監(jiān)牢的瞬間脖苏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工定踱, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留棍潘,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓崖媚,卻偏偏與公主長得像亦歉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子畅哑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,160評論 25 707
  • 目錄 什么是WPF肴楷? WPF的歷史? 為什么要用WPF及WPF作用 WPF與winForm區(qū)別荠呐? 什么是WPF赛蔫? ...
    灬52赫茲灬閱讀 5,812評論 2 11
  • 學(xué)習(xí)是厚積薄發(fā)的事呵恢,可能開始時看不到效果,但是它對你潛移默化圾结,最終使你脫胎換骨瑰剃。 有人說,學(xué)習(xí)跟銷售有關(guān)嗎筝野?讀書多...
    蝦妹閱讀 1,160評論 0 0
  • 曲筱綃說了一句話: 說出了現(xiàn)在很多人的生活狀態(tài)晌姚! 越是有錢的人粤剧,越滿世界的找生意! 只有沒錢的人挥唠,這也看不起抵恋,那也...
    湘子雅閱讀 135評論 0 0
  • 11111111
    Kevin_C__閱讀 165評論 0 0