Chromium之cef3的.net封装(定制化的浏览器)
第一次在博客园写文章,想跟大家分享一下一些关于Chromium的.net封装版本
从学校毕业后到现在也有一年半了,主要做.net方面的,winform和asp.net,MVC。期间维护过一个项目,用Winform的WebBrowser展现web网站项目,给用户更好的用户体验。后来听说了html5,很想试试,但由于WebBrowser是采用IE内核,么法子。找了一堆和浏览器相关的资料,当中也学到了不少。比如说Chrome Frame 并不能和WebBrowser相结合,FireFox的Gecko的binary感觉好冗余,也没有继续下去。后来看到了采用V8引擎的Chromium,执行javascript的速度快,而且binary也很精简,代码看着清爽,蛮喜欢的。于是采用了。
经过资料和第三方代码的整合之后,搭了这个项目
1. CefGlue项目是对libcef.dll的封装(libcef.dll是Chromium的核心,可以从开源项目Chromium的SVN下来,编译下来,我编译的version是1045)
2. Cef.Demo项目是引用CefGlue项目,主要调用CefGlue对libcef.dll封装的API,并在此基础上封装了一个WebBrowser基类
3. Cef.Demo.Common是帮助类库
4. Cef.Demo.WinForms(Winform主程序)
下面是Cef.Demo项目
其中类DemoCefApp是Cef3应用的核心,他主管着两类进程,一类进程是Browser进程,这类进程有着一系列的事件可供重写,
其中有V8引擎上下文初始化完成事件,代码如下
//V8引擎初始化完成,可重写 protected virtual void OnContextInitialized() { }
还有当Render进程(下面会讲到Render进程)创建时的事件,代码如下
//当一个新的渲染进程创建时 可重写 protected virtual void OnRenderProcessThreadCreated(CefListValue extraInfo) { }
另一类进程就是Render进程,同样他也有很多事件可供重写,当你打开chrome浏览网页,你会发现任务管理面有时会有很多chrome的进程,多出的那些进程就是Render进程。每当你打开一个新的tab或者页面,其所承载的网页会创建一个新的进程,即Render进程。当你关闭一个tab或者页面时,这个进程就会被kill掉,资源也会被再次回收。
其中有此进程创建完成的事件
//Render进程创建完成时 可重写 protected virtual void OnRenderThreadCreated(CefListValue extraInfo) { }
还有两个该进程生命周期的两个事件
//在浏览器创建完成时 可重写 protected virtual void OnBrowserCreated(CefBrowser browser) { } //在浏览器销毁之前调用 可重写 protected virtual void OnBrowserDestroyed(CefBrowser browser) { }
有在浏览器跳转之前的事件
//可重写
protected virtual bool OnBeforeNavigation(CefBrowser browser, CefFrame frame, CefRequest request, CefNavigationType navigationType, bool isRedirect) { //默认返回假 允许跳转,否则返回真来阻止跳转 return false; }
有页面的javascript上下文创建时的事件
//javascript上下文创建时 可重写
protected virtual void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context) { }
以上举例了这些事件,其实这些事件也可以形象的理解为博客园中的自定义自己的主页一样,编写一段html代码让其更丰富,更美观,而这就是我们想要的。我可以不用知道博客园是怎样将我输入的代码改变我的主页面的,但是我只要分别写好页头的html代码和页脚的,交给博客园就可以了。同样对于libcef.dll也可以将它看成是“博客园”。
上面的代码中出现了CefBrowser和CEfFrame这两个比较常用的类,以下是代码
public sealed unsafe partial class CefBrowser { public CefBrowserHost GetHost() { return CefBrowserHost.FromNative( cef_browser_t.get_host(_self) ); } public bool CanGoBack { get { return cef_browser_t.can_go_back(_self) != 0; } } public void GoBack() { cef_browser_t.go_back(_self); } public bool CanGoForward { get { return cef_browser_t.can_go_forward(_self) != 0; } } public void GoForward() { cef_browser_t.go_forward(_self); } public bool IsLoading { get { return cef_browser_t.is_loading(_self) != 0; } } public void Reload() { cef_browser_t.reload(_self); } public void ReloadIgnoreCache() { cef_browser_t.reload_ignore_cache(_self); } public void StopLoad() { cef_browser_t.stop_load(_self); } public int Identifier { get { return cef_browser_t.get_identifier(_self); } } public bool IsSame(CefBrowser that) { if (that == null) return false; return cef_browser_t.is_same(_self, that.ToNative()) != 0; } public bool IsPopup { get { return cef_browser_t.is_popup(_self) != 0; } } public bool HasDocument { get { return cef_browser_t.has_document(_self) != 0; } } public CefFrame GetMainFrame() { return CefFrame.FromNative( cef_browser_t.get_main_frame(_self) ); } public CefFrame GetFocusedFrame() { return CefFrame.FromNative( cef_browser_t.get_focused_frame(_self) ); } public CefFrame GetFrame(long identifier) { return CefFrame.FromNativeOrNull( cef_browser_t.get_frame_byident(_self, identifier) ); } public CefFrame GetFrame(string name) { fixed (char* name_str = name) { var n_name = new cef_string_t(name_str, name.Length); return CefFrame.FromNativeOrNull( cef_browser_t.get_frame(_self, &n_name) ); } } public int FrameCount { get { return (int)cef_browser_t.get_frame_count(_self); } } public long[] GetFrameIdentifiers() { var frameCount = FrameCount; var identifiers = new long[frameCount]; UIntPtr n_count = (UIntPtr)frameCount; fixed (long* identifiers_ptr = identifiers) { cef_browser_t.get_frame_identifiers(_self, &n_count, identifiers_ptr); if ((int)n_count != frameCount) throw new InvalidOperationException(); // FIXME: ... } return identifiers; } public string[] GetFrameNames() { var list = libcef.string_list_alloc(); cef_browser_t.get_frame_names(_self, list); var result = cef_string_list.ToArray(list); libcef.string_list_free(list); return result; } public bool SendProcessMessage(CefProcessId target, CefProcessMessage message) { if (message == null) throw new ArgumentNullException("message"); return cef_browser_t.send_process_message(_self, target, message.ToNative()) != 0; } }
public sealed unsafe partial class CefFrame { public bool IsValid { get { return cef_frame_t.is_valid(_self) != 0; } } public void Undo() { cef_frame_t.undo(_self); } public void Redo() { cef_frame_t.redo(_self); } public void Cut() { cef_frame_t.cut(_self); } public void Copy() { cef_frame_t.copy(_self); } public void Paste() { cef_frame_t.paste(_self); } public void Delete() { cef_frame_t.del(_self); } public void SelectAll() { cef_frame_t.select_all(_self); } public void ViewSource() { cef_frame_t.view_source(_self); } public void GetSource(CefStringVisitor visitor) { if (visitor == null) throw new ArgumentNullException("visitor"); cef_frame_t.get_source(_self, visitor.ToNative()); } public void GetText(CefStringVisitor visitor) { if (visitor == null) throw new ArgumentNullException("visitor"); cef_frame_t.get_text(_self, visitor.ToNative()); } public void LoadRequest(CefRequest request) { if (request == null) throw new ArgumentNullException("request"); cef_frame_t.load_request(_self, request.ToNative()); } public void LoadUrl(string url) { fixed (char* url_str = url) { var n_url = new cef_string_t(url_str, url != null ? url.Length : 0); cef_frame_t.load_url(_self, &n_url); } } public void LoadString(string content, string url) { fixed (char* content_str = content) fixed (char* url_str = url) { var n_content = new cef_string_t(content_str, content != null ? content.Length : 0); var n_url = new cef_string_t(url_str, url != null ? url.Length : 0); cef_frame_t.load_string(_self, &n_content, &n_url); } } public void ExecuteJavaScript(string code, string url, int line) { fixed (char* code_str = code) fixed (char* url_str = url) { var n_code = new cef_string_t(code_str, code != null ? code.Length : 0); var n_url = new cef_string_t(url_str, url != null ? url.Length : 0); cef_frame_t.execute_java_script(_self, &n_code, &n_url, line); } } public bool IsMain { get { return cef_frame_t.is_main(_self) != 0; } } public bool IsFocused { get { return cef_frame_t.is_focused(_self) != 0; } } public string Name { get { var n_result = cef_frame_t.get_name(_self); return cef_string_userfree.ToString(n_result); } } public long Identifier { get { return cef_frame_t.get_identifier(_self); } } public CefFrame Parent { get { return CefFrame.FromNativeOrNull( cef_frame_t.get_parent(_self) ); } } public string Url { get { var n_result = cef_frame_t.get_url(_self); return cef_string_userfree.ToString(n_result); } } public CefBrowser Browser { get { return CefBrowser.FromNative( cef_frame_t.get_browser(_self) ); } } public CefV8Context V8Context { get { return CefV8Context.FromNative( cef_frame_t.get_v8context(_self) ); } } public void VisitDom(CefDomVisitor visitor) { if (visitor == null) throw new ArgumentNullException("visitor"); cef_frame_t.visit_dom(_self, visitor.ToNative()); } }
CefFrame可以认为是一个iframe,有global对象,有脚本的上下文。一个页面至少会有一个CefFrame,在这里页面是和CefBrowser等同的。即他们的关系是CefBrowser至少会有一个CefFrame,一个CefFrame必须在CefBrowser下生存。
介绍了上面这些后 还有一个必须得说一下,那就是WebClient,如果说DemoCefApp是主人,拥有一切,那么WebClient就是管家了。
WebClient会对两个进程之间的通信消息进行接收,代码如下
//可重写
protected virtual bool OnProcessMessageReceived(CefBrowser browser, CefProcessId sourceProcess, CefProcessMessage message) { return false; }
其中的CefProcessId如下
public enum CefProcessId { Browser, Renderer, }
而发送消息已经在上面的CefBrowser中有这么一个方法 public bool SendProcessMessage(CefProcessId target, CefProcessMessage message) CefBrowser是会在两类进程的主线程上都会有这么一个实例的引用。当你在某些事件调用时,你就可以通过这两个方法轻松的在不同进程之间发送和接收消息了。
WebClient还包含了很多细节的可重写方法。
比如说下面这个方法
protected virtual CefRequestHandler GetRequestHandler() { return null; }
通过自定义CefRequestHandler之后我们就可以返回自定义CefRequestHandler的对象,每当我们打算请求的时候,可以对这个request请求进行处理,处理完成之后再将该请求经行发送,甚至可以取消这次的发送。
下面再看看自定义的WebBrowser。由于之前已经将CefBrowser定义为sealed类型,于是我不再继承他了,我只将他作为一个属性以便于使用。同样我也需要一个管家WebClient经行进程间的通信还有对请求Request、加载Load等等的支持,才能让我这个WebBrowser提供更需要的功能。下面是代码
public sealed class WebBrowser { private readonly object _owner; private readonly CefBrowserSettings _settings; private string _startUrl; private CefClient _client; private CefBrowser _browser; private IntPtr _windowHandle; private bool _created; public WebBrowser(object owner, CefBrowserSettings settings, string startUrl) { _owner = owner; _settings = settings; _startUrl = startUrl; } public string StartUrl { get { return _startUrl; } set { _startUrl = value; } } public CefBrowser CefBrowser { get { return _browser; } } public void Create(CefWindowInfo windowInfo) { if (_client == null) { _client = new WebClient(this); } CefBrowserHost.CreateBrowser(windowInfo, _client, _settings, StartUrl); } public event EventHandler Created; internal void OnCreated(CefBrowser browser) { if (_created) throw new InvalidOperationException("Browser already created."); _created = true; _browser = browser; var handler = Created; if (handler != null) { handler(this, EventArgs.Empty); } } internal void Close() { if (_browser != null) { _browser.Dispose(); _browser = null; } } public event EventHandler<TitleChangedEventArgs> TitleChanged; internal void OnTitleChanged(string title) { var handler = TitleChanged; if (handler != null) { handler(this, new TitleChangedEventArgs(title)); } } public event EventHandler<AddressChangedEventArgs> AddressChanged; internal void OnAddressChanged(string address) { var handler = AddressChanged; if (handler != null) { handler(this, new AddressChangedEventArgs(address)); } } public event EventHandler<TargetUrlChangedEventArgs> TargetUrlChanged; internal void OnTargetUrlChanged(string targetUrl) { var handler = TargetUrlChanged; if (handler != null) { handler(this, new TargetUrlChangedEventArgs(targetUrl)); } } public event EventHandler<LoadingStateChangedEventArgs> LoadingStateChanged; internal void OnLoadingStateChanged(bool isLoading, bool canGoBack, bool canGoForward) { var handler = LoadingStateChanged; if (handler != null) { handler(this, new LoadingStateChangedEventArgs(isLoading, canGoBack, canGoForward)); } } public event EventHandler<BeforeResourceLoadedEventArgs> BeforeResourceLoaded; internal void OnBeforeResourceLoaded(CefRequest request, long frameIdentifier) { var handler = BeforeResourceLoaded; if (handler != null) { handler(this, new BeforeResourceLoadedEventArgs(request, frameIdentifier)); } } }
当中使用了很多自定义的事件,比如说刚刚自定义的CefRequestHandler在处理特定的请求时给自定义的WebBrowser一个信号,于是使用了事件的方式。
今天太晚了。。。打算下次在接着分享
可以先看一下几张截图。
下面是winform的项目引用以上的项目,采用了第三方的Razor视图引擎,能够满足离线应用和在线应用的需求了
看一下运行效果吧
登陆成功后(异步登陆是ajax的方式并对ajax请求经行捕获,使用了jquery。。惊奇的发现该请求的Method是options,不是post和get,怪不得一开始没捕获到。小插曲啦,嘻嘻)