天天看點

Qt浏覽器開發:關于CEF開發知識點以及QCef開發原理與使用

開發環境:

VS2015+Qt5.9

關于CEF

CEF全稱是Chromium Embedded Framework,它是Chromium的Content API的封裝庫,基于Google Chromium 的開源項目,而Google Chromium項目主要是為Google Chrome應用開發的,而CEF的目标則是為第三方應用提供可嵌入浏覽器支援

主要組成分為

Chromium:基礎,網絡堆棧,線程,消息機制,log,程序控制,生成Web browser。

WebKit:提供DOM解析,布局,事件處理,渲染,HTML5JS的API。

V8:JS引擎。

Skia:2D圖形庫。

Angle:3D圖形轉換,和DirectX有關。

目前CEF分為CEF1,CEF2,CEF3,其中前者使用的是單程序架構,後兩者是多程序架構。

在開發使用中一般都是基于CEF3開發。

CEF3是基于Chomuim Content API多程序構架的下一代CEF,擁有下列優勢:

1.改進的性能和穩定性(JavaScript和插件在一個獨立的程序内執行)。

2.支援Retina顯示器。

3.支援WebGL和3D CSS的GPU加速。

4.類似WebRTC和語音輸入這樣的前衛特性。

5.通過DevTools遠端調試協定以及ChromeDriver2提供更好的自動化UI測試。

6.更快獲得目前以及未來的Web特性和标準的能力。

CEF使用(QCef封裝)

單一執行體(Single Executable)

以windows平台為例子

在使用CEF3的時候,需要在主程序中啟動CEF
CefEnableHighDPISupport();
//入口函數(Entry-Point Function)
HINSTANCE hInstance = static_cast<HINSTANCE>(GetModuleHandle(nullptr));
CefMainArgs main_args(hInstance);
void* sandbox_info = nullptr; //沙盒資訊為空
CefRefPtr<MyRenderProcessHandler> app(new MyRenderProcessHandler);//處理程序相關的回調。
 int exit_code = CefExecuteProcess(main_args, app.get(), sandbox_info);
    if (exit_code >= 0) {
        // 子流程已經完成,傳回
        return exit_code;
    }
CefSettings settings;
settings.no_sandbox = true;  //關閉沙盒模式
settings.multi_threaded_message_loop = true; //多線程消息循環
settings.log_severity = LOGSEVERITY_DISABLE;//日志
//settings.windowless_rendering_enabled = true; //開啟離屏模式
CefInitialize(main_args, settings, app_r, sandbox_info);

//啟動CEF消息循環,運作後會阻塞到CefQuitMessageLoop()被喚醒
CefRunMessageLoop();

//關閉CEF
CefShutdown();
           

CEF3常用類和接口

一:CefClient:回調管理類

CefClient提供通路browser-instance-specific的回調接口,單執行個體CefClient可以共數任意數量的浏覽器程序。

比如處理Browser的生命周期,右鍵菜單,對話框,通知顯示, 拖曳事件,焦點事件,

鍵盤事件等等。如果沒有對某個特定的處理接口進行實作會造成什麼影響,請檢視

cef_client.h檔案中相關說明。

舉例代碼說明(目前小編用到的開發代碼)

class CefBrowserHandler: public QObject,
	public CefClient,
	public CefDisplayHandler,
	public CefLifeSpanHandler,
	public CefFocusHandler,
	public CefLoadHandler//,
	//public CefRenderHandler  //離屏渲染(關于離屏渲染,後續介紹)
{
	Q_OBJECT

public:
	CefBrowserHandler();

public:
	// CefClient methods:
	virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() OVERRIDE { return this;}
	virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE { return this;}
	virtual CefRefPtr<CefLoadHandler> GetLoadHandler() OVERRIDE { return this; }
	virtual CefRefPtr<CefFocusHandler> GetFocusHandler() OVERRIDE { return this; }
	//virtual CefRefPtr<CefRenderHandler> GetRenderHandler() OVERRIDE { return this; }
	
	// CefLifeSpanHandler methods:
	virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
	virtual bool DoClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
	virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
	virtual bool OnBeforePopup(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		const CefString& target_url,
		const CefString& target_frame_name,
		WindowOpenDisposition target_disposition,
		bool user_gesture,
		const CefPopupFeatures& popupFeatures,
		CefWindowInfo& windowInfo,
		CefRefPtr<CefClient>& client,
		CefBrowserSettings& settings,
		bool* no_javascript_access)OVERRIDE;
		
	//用來發送一些網頁處理的信号
	// CefLoadHandler methods:
	virtual void OnLoadError(CefRefPtr<CefBrowser> browser,
		CefRefPtr<CefFrame> frame,
		ErrorCode errorCode,
		const CefString& errorText,
		const CefString& failedUrl) OVERRIDE;
	virtual void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,bool isLoading,bool canGoBack,bool canGoForward) OVERRIDE;
	virtual void OnLoadStart(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, TransitionType transition_type) OVERRIDE;
	virtual void OnLoadEnd(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int httpStatusCode) OVERRIDE;

	// CefFocusHandler methods:
	virtual void OnGotFocus(CefRefPtr<CefBrowser> browser) OVERRIDE;
    virtual bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefProcessId source_process, CefRefPtr<CefProcessMessage> message);
	
	// Member accessors.
	void CloseAllBrowsers(bool forceClose);
	CefRefPtr<CefBrowser> GetBrower() { return m_Browser; }
	bool IsClosing() { return m_bIsClosing; }

	// CefRenderHandler methods:
	/*virtual bool GetViewRect(CefRefPtr<CefBrowser> browser, CefRect& rect) OVERRIDE;
	virtual void OnPaint(CefRefPtr<CefBrowser> browser,
		PaintElementType type,
		const RectList& dirtyRects,
		const void* buffer,
		int width,
		int height) OVERRIDE;
	 virtual bool GetScreenPoint(CefRefPtr<CefBrowser> browser,
         int viewX,
         int viewY,
         int& screenX,
         int& screenY) OVERRIDE;
      virtual void OnCursorChange(CefRefPtr<CefBrowser> browser,
         CefCursorHandle cursor,
         CursorType type,
         const CefCursorInfo& custom_cursor_info) OVERRIDE;*/
	
signals:
	void loadStarted();
	void loadFinished(bool loadSuccess);
	void browserCreated();
	void urlChanged(const QString& url);
	void titleChanged(const QString& title);
	void loadingStateChanged(bool isLoading, bool canGoBack, bool canGoForward);
	void webViewGotFocus();
    void recvRenderMsg(const QString& msg);
	
private:
	CefRefPtr<CefBrowser> m_Browser;
	int m_BrowserId;
	int m_BrowserCount;
	bool m_bIsClosing;	
	typedef std::list<CefRefPtr<CefBrowser>> BrowserList;
	BrowserList _browserList;
	
	IMPLEMENT_REFCOUNTING(CefBrowserHandler);
	IMPLEMENT_LOCKING(CefBrowserHandler);

};
           

1.Browser的生命周期從執行 CefBrowserHost::CreateBrowser() 或者CefBrowserHost::CreateBrowserSync()開始

建立代碼,以windows平台為例子

//自定義此結構體來管理浏覽器行為
CefBrowserSettings cSettings;
cSettings.file_access_from_file_urls = STATE_ENABLED;  //url檔案能否通路其他url檔案
cSettings.universal_access_from_file_urls = STATE_ENABLED;  //url檔案能否通路所有url
cSettings.javascript_access_clipboard = STATE_ENABLED; //js是否可以通路剪貼闆
cSettings.plugins = STATE_DISABLED;  //是否加載插件
//cSettings.windowless_frame_rate = 60; //設定幀率,預設值是30
//根據視窗id來确定浏覽器渲染位置
CefWindowInfo info;
CefWindowHandle hWnd = (CefWindowHandle)this->winId();
RECT rect;
rect.left = 0;
rect.top = 0;
rect.right = this->width()*qApp->devicePixelRatio();
rect.bottom = this->height()*qApp->devicePixelRatio();
info.SetAsChild(hWnd, rect);
QString url = "www.baidu.com";
//初始化CefClient子類,其中CefLifeSpanHandler類提供了管理Browser生命周期所必需的回調
CefRefPtr<CefBrowserHandler> browserHandler = GetBrowserHandler(browserHandlerIndex); 
//建立浏覽器
CefBrowserHost::CreateBrowser(info, browserHandler.get(), CefString(url.toStdWString()), _settings, NULL);
           

2.當Browser對象建立後OnAfterCreated() 方法立即執行,宿主程式可以用這個方法來保持對Browser對象的引用。

void CefBrowserHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser)
{
	CEF_REQUIRE_UI_THREAD();
	//保護成員不受多線程上的通路
	AutoLock lock_scope(this);
	if (!_browser.get()) {
		// 建立浏覽器成功後彈出浏覽器主視窗
		_browser = browser;
		_browserId = browser->GetIdentifier();
		//發送信号,處理某些業務邏輯
		emit browserCreated();
	}
	else if (browser->IsPopup()) {
		// 如果浏覽器視窗已經彈出,加入list,友善後續管理(保證隻能在CEF UI線程通路)
		_browserList.push_back(browser);
	}
	//記錄浏覽器個數
	++_browserCount;
}
           

3.Browser的生命周期從執行CefBrowserHost::CloseBrowser()銷毀Browser對象結束

Browser對象的關閉事件來源于他的父視窗的關閉方法(比如,在父視窗上點選X控鈕。)。父視窗需要調用 CloseBrowser(false) 并且等待作業系統的第二個關閉事件來決定是否允許關閉。

void CefBrowserHandler::CloseAllBrowsers(bool forceClose)
{
	qInfo() << "CloseAllBrowsers::" << __FUNCTION__;
	if (!CefCurrentlyOn(TID_UI)) {
		// 在UI線程中執行
		CefPostTask(TID_UI, base::Bind(&CefBrowserHandler::CloseAllBrowsers, this, forceClose));
		return;
	}
	if (_browser == NULL)
		return;
	if (!_browserList.empty()) {
		//關閉浏覽器清單存在的浏覽器
		BrowserList::const_iterator it = _browserList.begin();
		for (; it != _browserList.end(); ++it)
			(*it)->GetHost()->CloseBrowser(forceClose);
		return;
	}
	if (_browser.get()) {
		// 請求關閉浏覽器
		_browser->GetHost()->CloseBrowser(forceClose);
	}
}
           

DoClose方法設定m_blsClosing 标志位為true,并傳回false以再次發送作業系統的關閉事件。

bool CefBrowserHandler::DoClose(CefRefPtr<CefBrowser> browser)
{
	qInfo() << "DoClose::" << __FUNCTION__;
	CEF_REQUIRE_UI_THREAD();
	//保護成員不受多線程上的通路
	AutoLock lock_scope(this);
	
	if (browser->GetIdentifier() == _browserId) {
		_isClosing = true;
	}
	//再次發送作業系統的關閉事件
	return false;
}
           

當作業系統捕捉到第二次關閉事件,它才會允許父視窗真正關閉。該動作會先觸發OnBeforeClose()回調,請在該回調裡釋放所有對浏覽器對象的引用

void CefBrowserHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser)
{
	qInfo() << "OnBeforeClose::" << __FUNCTION__;
	CEF_REQUIRE_UI_THREAD();
		//保護成員不受多線程上的通路
	AutoLock lock_scope(this);

	if (_browserId == browser->GetIdentifier()) {
		_browser = nullptr;
	}
	else if (browser->IsPopup()) {
		//清空浏覽器清單并關閉銷毀浏覽器對象
		BrowserList::iterator bit = _browserList.begin();
		for (; bit != _browserList.end(); ++bit) {
			if ((*bit)->IsSame(browser)) {
				_browserList.erase(bit);
				break;
			}
		}
	}
	if (--_browserCount == 0) {
		// 當所有浏覽器視窗已關閉退出時應用程式消息循環.
		CefQuitMessageLoop();
	}
}
           

二:CefApp

CefApp接口提供了不同程序的可定制回調函數,包含與程序,指令行參數,代理,資源管理相關的回調類,一些功能是由所有程序共享的,有些必須實作浏覽器的過程中,必須在渲染過程中執行,可以讓開發者定制屬于自己的邏輯。

其中重要的回調函數如下:

1.OnBeforeCommandLineProcessing 提供了以程式設計方式設定指令行參數的機會。

2.OnRegisterCustomSchemes 提供了注冊自定義schemes的機會。

3.CefBrowserProcessHandler 傳回定制Browser程序的Handler,該Handler包括了OnContextInitialized這樣的回調。

4.CefRenderProcessHandler 傳回定制Render程序的Handler,該Handler包含了JavaScript相關的一些回調以及消息處理的回調。

舉例代碼說明(小編所用到的回調類)

//CefBrowserProcessHandler
class CefAppHandler : public CefApp,
    public CefBrowserProcessHandler
{

public:
	CefAppHandler();

public:
    // CefApp methods:
    virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() OVERRIDE {
        return this;
    }	

private:
	IMPLEMENT_REFCOUNTING(CefAppHandler);
};

//CefRenderProcessHandler
class MyRenderProcessHandler :public CefApp,
    public CefRenderProcessHandler
{

public:
    MyRenderProcessHandler();
    CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() OVERRIDE
    {
        return this;
    }

    void OnContextCreated(
        CefRefPtr<CefBrowser> browser,
        CefRefPtr<CefFrame> frame,
        CefRefPtr<CefV8Context> context);

private:

    IMPLEMENT_REFCOUNTING(MyRenderProcessHandler);
};
           

詳細定制邏輯功能不詳說了,因功能而異實作對應邏輯。

三:CefBrowser和CefFrame

CefBrowser管理renderer程序中執行浏覽相關的類,包括網頁前進後退,曆史導航,加載字元串和請求,發送編輯指令,提取text/html内容,來源檢索,加載請求等。

CefFrame用于加載特定url,在該運作環境下執行JavaScript代碼等。

CefBrowser和CefFrame對象被用來發送指令給浏覽器以及在回調函數裡擷取狀态資訊。每個CefBrowser對象包含一個主CefFrame對象,主CefFrame對象代表頁面的頂層frame。

同時每個CefBrowser對象可以包含零個或多個的CefFrame對象,分别代表不同的子Frame,例如,一個浏覽器加載了兩個iframe,則該CefBrowser對象擁有三個CefFrame對象(頂層frame和兩個iframe)。

常見的網頁操作舉例代碼如下:

//在浏覽器的主frame加載一個特定url
browser->GetMainFrame()->LoadURL("www.baidu.com");
//浏覽器頁面回退
browser->GoBack();
//浏覽器頁面向前
browser->GoForward();
//浏覽器頁面重新整理
browser->Reload();
//浏覽器頁面停止
browser->StopLoad();
//浏覽器視窗的原生句柄
CefWindowHandle window_handle = browser->GetHost()->GetWindowHandle();
//從主frame裡擷取HTML内容
class Visitor : public CefStringVisitor 
{
public:
	Visitor() {}
	virtual void Visit(const CefString& string) OVERRIDE {
		// xxx(); 某些邏輯操作
	}
IMPLEMENT_REFCOUNTING(Visitor);
};
browser->GetMainFrame()->GetSource(new Visitor());
           

四:V8引擎

v8 引擎用于高效解析和執行 JavaScript。

v8 引擎設計了高效的垃圾回收機制,以保證快速的對象記憶體配置設定、短暫的垃圾回收暫停、無記憶體碎片。

CEF3提供支援V8Extension的接口,但是這有兩個限制

第一,v8 extension僅在Renderer程序使用。

第二,僅在沙箱模型關閉時使用。

V8引擎的功能比較多,目前小編隻用到了CefV8Handler類與JavaScript互動功能

CefV8Handler 是一個純接口類,隻有一個方法,你可以繼承它,并提供相應的實作

class MyV8Handler :public CefV8Handler 
{
public:
  MyV8Handler();
  virtual bool Execute(const CefString& name,
                       CefRefPtr<CefV8Value> object,
                       const CefV8ValueList& arguments,
                       CefRefPtr<CefV8Value>& retval,
                       CefString& exception) OVERRIDE;

  void SendMsgToBrowser(CefString str);
public:

  IMPLEMENT_REFCOUNTING(MyV8Handler);
};

//與js進行通信互動,還需要用到CefV8Context類和CEF 類型:CefV8Value
CefRefPtr<CefV8Context> context
CefRefPtr<CefV8Value> object = context->GetGlobal();
// 建立MyV8Handler對象
CefRefPtr<MyV8Handler> handler = new MyV8Handler();
CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("ReceivedMsgFromJS", handler);
 object->SetValue("ReceivedMsgFromJS", func, V8_PROPERTY_ATTRIBUTE_NONE);
 
//發送消息
void QCefWebView::SendMsgToPage(const QString &msg)
{
    CefRefPtr<CefFrame> frame =GetBrowser(browserHandlerIndex)->GetMainFrame();
    QString datastr;
    datastr="SigSendMessageToJS('";
    datastr+=msg;
    datastr+="')";
    CefString code;
    code.FromString(datastr.toStdString());
    frame->ExecuteJavaScript(code, frame->GetURL(), 0);
}
bool MyV8Handler::Execute(const CefString &name, CefRefPtr<CefV8Value> object, const CefV8ValueList &arguments, CefRefPtr<CefV8Value> &retval, CefString &exception)
{
    if (name == "ReceivedMsgFromJS") {
        if (arguments.size() == 1) {
            CefString strFromWeb = arguments.at(0)->GetStringValue();
            std::string stdstr=strFromWeb.ToString();
            QString str=QString::fromLocal8Bit(strFromWeb.ToString().c_str());
			SendMsgToBrowser(strFromWeb);
            retval = CefV8Value::CreateString(strFromWeb);
        }
        else {
            retval = CefV8Value::CreateInt(0);
        }
        return true;
    }
    return false;
}
void MyV8Handler::SendMsgToBrowser(CefString str)
{
    CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("vvMsg");
    CefRefPtr<CefListValue> args = msg->GetArgumentList();
    args->SetSize(1);
    args->SetString(0, str);
    CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg);
}
 
//接收消息
//OnProcessMessageReceived在Render程序收到程序間消息時被調用
bool CefBrowserHandler::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefProcessId source_process, CefRefPtr<CefProcessMessage> message)
{
    const std::string& messageName = message->GetName();
        if (messageName == "vvMsg") //messageName要和MyV8Handler類SendMsgToBrowser函數中定義的一樣
        {
            CefRefPtr<CefListValue> args = message->GetArgumentList();
            CefString string0 = args->GetString(0);
           // QString res=QString::fromLocal8Bit(string0.ToString().c_str()); 中文會亂碼
           QString res = QString::fromStdWString(string0.c_str());
            emit recvRenderMsg(res);
            return true;
        }
        return false;
}
           

五:其他類

常見的都在以上代碼中已定義,其他的可以查詢文檔詳解,不例舉了。

CEF官方文檔

目前還在深入研究學習CEF,文章有諸多漏洞,望見諒,望指正,感謝~

CEF官網位址

CEF官方論壇

參考文檔:https://github.com/fanfeilong/cefutil/blob/master/doc/CEF%20General%20Usage-zh-cn.md#off-screen-rendering

内附小編浏覽器效果

Qt浏覽器開發:關于CEF開發知識點以及QCef開發原理與使用

繼續閱讀