1. 程式人生 > >Cef功能開發經驗總結

Cef功能開發經驗總結

這是我開發Cef功能時對踩過的坑,進行的總結,話說Cef坑真的不少。好在踩完後用起來還是挺爽的。最終的程式碼可以下載網易雲信PC Demo C++原始碼點我跳轉

資料準備

這是我整合過程中查到的一些資料,包括了Cef開發的各方面資料

在除錯Cef時需要Cef的pdb和原始碼:

Cef基本結構

CefApp介面

CefApp介面提供了不同程序的可定製回撥函式,每一個程序對應一個CefApp介面。CefBrowserProcessHandler對應瀏覽器程序的回撥,CefRenderProcessHandler對應渲染程序的回撥。我們應該繼承CefApp、CefBrowserProcessHandler、CefRenderProcessHandler介面。如果完全使用多程序模式,可以分別在瀏覽器程序和渲染程序裡分開繼承介面

CefApp::OnBeforeCommandLineProcessing方法裡可以附加傳入給Cef的命令列引數,這裡可以附加很多控制引數

CefRenderProcessHandler::OnWebKitInitialized方法可以在渲染程序初始化時用來註冊JS擴充套件程式碼,實現C++與JS互動

CefRenderProcessHandler::OnFocusedNodeChanged方法可以檢測當前獲取到焦點html元素,獲取到一些元素資訊可以通過程序通訊傳送給瀏覽器程序來輔助做進一步的判斷

CefRenderProcessHandler::OnProcessMessageReceived方法用於接收瀏覽器程序發來的訊息,在做C++與JS互動時會用到

CefClient介面

每一個CefBrowser物件會對應一個CefClient介面,用於處理瀏覽器頁面的各種回撥資訊,包括了Browser的生命週期,右鍵選單,對話方塊,狀態通知顯示,下載事件,拖曳事件,焦點事件,鍵盤事件,離屏渲染事件。隨著Cef版本的更新這些介面也會擴充套件和更新,多數對Cef進行行為控制的方法都集中在這些介面,如果對Cef有新的功能需求,一般都可以先翻翻這些介面中有沒有提供相關功能

CefClient::OnProcessMessageReceived方法用於接收渲染程序發到的訊息,在做C++與JS互動時會用到

CefSettings結構體

CefSettings結構體定義了Cef的全域性配置資訊,比如指定單程序模式、指定渲染子程序路徑、設定localstorage路徑、設定日誌等級、Cef資原始檔路徑。其中對於專案最重要的欄位是single_process、multi_threaded_message_loop、windowless_rendering_enabled,分別用於指定單程序模式、多執行緒渲染模式、離屏渲染模式。

相容現有的訊息迴圈

如果是UI執行緒訊息迴圈構架較簡單的專案,可以直接呼叫CefRunMessageLoop來使用Cef自帶的訊息迴圈,它會阻塞執行緒直到呼叫了CefQuitMessageLoop函式,CefRunMessageLoop是相容傳統的Win32訊息迴圈的。

不過NIM專案底層是使用谷歌base庫的多執行緒構架,所以沒法直接使用CefRunMessageLoop。(PS:實際上Cef的底層訊息迴圈也是谷歌的base庫)

要讓NIM的訊息迴圈相容Cef訊息迴圈,有兩種方法。

第一種方法

第一種方法是使用CefDoMessageLoopWork函式代替CefRunMessageLoop來完全訊息訊息迴圈。CefDoMessageLoopWork函式的作用是讓Cef執行一次訊息迴圈,這個函式不會阻塞執行緒,所以需要在我們現有的訊息迴圈裡的適當情況下主動去呼叫CefDoMessageLoopWork函式,如果呼叫的太頻繁會很消耗CPU,如果呼叫頻率太低會導致Cef來不及處理內部訊息,讓Cef介面反映變慢,所以這個函式的呼叫時機很重要。

因為CefDoMessageLoopWork函式應該在原本的訊息迴圈中呼叫,而base庫的UI執行緒訊息迴圈是封裝好的。這裡首先說一下定製base庫訊息迴圈的方法。在WinMain入口函式裡呼叫UI訊息迴圈的程式碼如下:

{
    MainThread thread; // 建立主執行緒
    thread.RunOnCurrentThreadWithLoop(nbase::MessageLoop::kUIMessageLoop); // 執行主執行緒迴圈
}
  • 1
  • 2
  • 3
  • 4

在RunOnCurrentThreadWithLoop方法的第二個引數裡可以指定一個訊息分派器指標dispatcher,dispatcher繼承自nbase::Dispatcher。base庫中的UI訊息迴圈程式碼如下:

PreProcessMessage(msg);

if (state_->dispatcher)
{
    if (!state_->dispatcher->Dispatch(msg))
        state_->should_quit = true;
}
else
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

PostProcessMessage(msg);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如果我們指定了RunOnCurrentThreadWithLoop方法的第二個引數,就不會呼叫原本的訊息迴圈了,所以可以在這個dispatcher裡定製訊息迴圈。我實現CefMessageLoopDispatcher類並重寫Dispatch介面。

BOOL CefMessageLoopDispatcher::IsIdleMessage(const MSG* pMsg)
{
    switch (pMsg->message)
    {
    case WM_MOUSEMOVE:
    case WM_NCMOUSEMOVE:
    case WM_PAINT:
        return FALSE;
    }

    return TRUE;
}

bool CefMessageLoopDispatcher::Dispatch(const MSG &msg)
{
    static BOOL bDoIdle = TRUE;

    TranslateMessage(&msg);
    DispatchMessage(&msg);

    if (IsIdleMessage(&msg))
    {
        bDoIdle = TRUE;
    }

    while (bDoIdle && !::PeekMessage(const_cast<MSG*>(&msg), NULL, 0, 0, PM_NOREMOVE))
    {
        CefDoMessageLoopWork();
        bDoIdle = FALSE;
    }

    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

在定製訊息迴圈裡,如果判斷當前訊息佇列為空並且剛才處理的訊息不會指定的幾個訊息,就去呼叫CefDoMessageLoopWork函式。WM_PAINT、WM_MOUSEMOVE等訊息的處理比較複雜,所以不在這裡呼叫CefDoMessageLoopWork函式

這個方法基本可以使用,但是還存在一些問題,這裡CefDoMessageLoopWork函式的呼叫機制還不夠好,Cef介面不夠順暢,而且因為Cef與專案base庫的衝突,導致在程式結束時有些問題。這個方法有待優化

第二種方法

CefSettings結構體的multi_threaded_message_loop(多執行緒訊息迴圈)為false時,可以呼叫CefRunMessageLoop或者CefDoMessageLoopWork函式來觸發Cef訊息迴圈,這時瀏覽器程序的UI執行緒就是呼叫CefRunMessageLoop或者CefDoMessageLoopWork函式的執行緒。如果CefSettings結構體的multi_threaded_message_loop為true時。瀏覽器程序的UI執行緒是另外的執行緒。設定multi_threaded_message_loop為true則使用多執行緒訊息迴圈。

通過對比Cef Demo的多執行緒訊息迴圈程式碼,可以確定在NIM專案中直接開啟多執行緒訊息迴圈,不需要修改現有訊息迴圈程式碼就可以正常使用Cef了。不過需要注意的是,使用多執行緒訊息迴圈後某些函式就無法使用了,比如CreateBrowserSync,這種函式要求必須在Cef的UI執行緒呼叫。

另外,在很多版本的Cef裡,如果開啟了多執行緒訊息迴圈,會導致程式在結束時觸發中斷,這屬於Cef的bug,不過在release版本的Cef中沒有問題。應該在專案中使用這個方法。不過使用了多執行緒訊息迴圈後,很多Cef物件觸發的回撥函式,都是在Cef的UI執行緒而不是我們的UI執行緒,所以這時操作我們的UI執行緒就比較麻煩,要注意一些多執行緒問題,儘量把操作轉發到我們的UI執行緒,不轉發的話必須確定所操作的程式碼不會影響我們的UI執行緒,切記!

CefClient介面介紹

CefLifeSpanHandler

CefBrowser物件的生命週期事件的回撥介面。

  • OnAfterCreated:當呼叫CreateBrowser函式建立瀏覽器物件後會立馬觸發這個回撥,在這裡可以儲存瀏覽器物件的指標
  • DoClose:當呼叫CloseBrowser函式後觸發這個回撥
  • OnBeforeClose:當瀏覽器物件即將銷燬時會觸發這個回撥,在這裡一定要釋放所有對CefBrowser物件的引用,否則會導致程式無法退出。切記這個坑。
  • OnBeforePopup:當單擊了網頁中會彈出新視窗的連結時,會觸發這個回撥。我們的專案裡應該禁止新視窗的彈出,而在原控制元件中跳轉連結

CefRenderHandler

要使用離屏渲染功能,就必須要實現這個介面類。因為專案中使用的duilib庫,目前使用分層窗體機制實現異形窗體效果,不支援顯示子視窗只能自繪控制元件,所以沒法使用比較簡單的子視窗的形式顯示Cef瀏覽器物件。只能使用離屏渲染方法,離屏渲染的資料會通過CefRenderHandler介面回撥。

首先必須要開啟CefSettings結構體的windowless_rendering_enabled欄位

  • GetRootScreenRect:瀏覽器物件建立後觸發的回撥,返回最外層窗體在螢幕中的位置
  • GetViewRect:在瀏覽器物件初始化後,或者瀏覽器大小改變時,觸發這個回撥來獲取瀏覽器物件的位置。因為瀏覽器物件會平鋪滿整個控制元件,所以這裡返回控制元件的位置。其中返回的左上位置要準確,否則Cef在處理一些座標資訊時會出錯
  • GetScreenPoint:在這裡把傳入的座標值,由客戶區座標轉換為螢幕座標
  • OnCursorChange:當需要修改滑鼠游標時觸發這個回撥
  • OnPaint:當瀏覽器物件有新的渲染資料後,會觸發這個回撥,包含了髒區和渲染資料。應該儲存這些資料,然後在適當的時候貼到目標窗體上
  • OnPopupShow:當瀏覽器中要彈出內部對話方塊時(比如彈出一個下拉選單),觸發這個回撥,通知要顯示或者隱藏彈出框
  • OnPopupSize:當瀏覽器中要彈出內部對話方塊時,觸發這個回撥,通知彈出框的位置和大小

離屏渲染的實現

離屏渲染的效率不如真視窗渲染,如果不是必須要離屏渲染的情況,還是用真視窗比較好。CefControl控制元件實現了duilib嵌入Cef瀏覽器物件。

在控制元件初始化觸發Init函式時,呼叫CreateBrowser函式建立CefBrowser物件,這會觸發CefRenderHandler::GetViewRect回撥,在這個回撥裡返回控制元件的位置。隨後網頁第一次渲染時觸發CefRenderHandler::OnPaint回撥。

繪製渲染資料的流程

  1. 當網頁渲染資料改變、或者我們主動呼叫了CefBrowser物件的Invalidate方法時,會觸發CefRenderHandler::OnPaint回撥。
  2. 我寫了一個記憶體點陣圖緩衝類MemoryDC來儲存Cef傳來的渲染資料,在CefControl控制元件中dc_cef_成員變數負責儲存渲染資料。在CefRenderHandler::OnPaint回撥裡,根據渲染資料初始化dc_cef_,然後根據髒區把渲染資料拷貝到dc_cef_中。
  3. 資料拷貝完之後,呼叫CefControl控制元件的Invalidate方法通知窗體重繪控制元件
  4. 在CefControl控制元件的Paint方法裡,把dc_cef_的點陣圖資料拷貝到duilib傳入的HDC中

CefControl對事件的處理

修改CefBrowser尺寸
在離屏渲染模式下,無法直接修改CefBrowser物件的尺寸。CefControl控制元件重寫SetPos函式,在這裡呼叫CefBrowser物件的WasResized介面通知CefBrowser物件需要改變尺寸,之後GetViewRect介面會被觸發,這時依然是返回CefControl控制元件的位置就可以了。之後OnPaint介面會被自動觸發,按照前一節的流程進行一次渲染資料的重新整理

設定CefBrowser隱藏(顯示)
CefControl控制元件重寫SetVisible函式和SetInternVisible函式,在這裡呼叫CefBrowser物件的WasHidden介面通知CefBrowser物件隱藏或顯示

對系統訊息的處理

  1. 在控制元件初始化觸發Init函式時,呼叫窗體類的AddMessageFilter函式把自己註冊到窗體的訊息過濾佇列裡。CefControl控制元件繼承IUIMessageFilter介面類並重寫MessageHandler函式。當系統訊息進入窗體後會依次呼叫訊息過濾佇列指標來過濾訊息。在MessageHandler函式裡處理我們感興趣的訊息,其他訊息並不過濾
  2. 處理各種滑鼠類訊息時,判斷如果滑鼠不在控制元件範圍內則不處理相關訊息。獲取當前滑鼠的座標,因為CefBrowser的座標值是以自身左上角作為原點的,所以獲取的滑鼠座標要減去CefControl控制元件的左上角座標值。其中處理ButtonDown、ButtonUp、MouseMove訊息時,不會中斷訊息繼續傳遞給窗體,這裡需要讓duilib窗體類處理SetCapture、ReleaseCapture等函式
  3. 處理鍵盤訊息時,判斷當前控制元件是否獲取焦點,只處理有焦點的情況
  4. WM_SETCURSOR訊息處理,在MessageHandler函式攔截WM_SETCURSOR訊息,直接呼叫窗體類的預設訊息處理函式,不讓duilib處理這個訊息。CefRenderHandler::OnCursorChange介面會修改滑鼠游標並修改窗體的預設游標樣式,而duilib處理WM_SETCURSOR訊息時會另外修改游標,所以需要攔截

渲染Popup彈出框

  1. 瀏覽器中,彈出框的渲染資料是需要自己額外處理的。如下拉選單等彈出框,否則瀏覽器中不會顯示出彈出框
  2. 當需要顯示彈出框時,CefRenderHandler::OnPopupSize介面會傳入彈出框的位置和尺寸等資料,在這裡把資料儲存到rect_popup_成員變數
  3. 之後會觸發CefRenderHandler::OnPaint回撥,並且渲染型別會被指定為彈出框型別PET_POPUP。這時把彈出框的渲染資料儲存到MemoryDC型別的dc_cef_popup_成員變數中
  4. 資料拷貝完之後,呼叫CefControl控制元件的Invalidate方法通知窗體重繪控制元件
  5. 在CefControl控制元件的Paint方法裡,把dc_cef_popup_的點陣圖資料按照rect_popup_的資訊拷貝到duilib傳入的HDC中
  6. 當彈出框消失時,觸發CefRenderHandler::OnPopupShow介面,這裡重置rect_popup_的資訊,並且通知CefBrowser重新整理頁面

多程序渲染

Cef3支援多程序和單程序渲染,但是單程序渲染不夠穩定,只應該在Debug模式下作為除錯目的使用。在Cef3.1916等好幾個版本中,除錯狀態下使用單程序模式,當程式初始化或者退出時,會觸發中斷。但是在多程序模式下沒有問題。官方也明確說明不推薦使用單程序模式

CefManager類實現了Cef3的初始化和銷燬功能。初始化函式Initialize裡呼叫的CefExecuteProcess函式會檢測當前的程序型別,如果是瀏覽器程序則函式會直接返回,在其他程序的話這個函式會阻塞直接程序銷燬。

ClientApp類繼承CefBrowserProcessHandler和CefRenderProcessHandler,可以同時處理瀏覽器程序和渲染程序的訊息。原本多程序模式中,瀏覽器程序和渲染程序可以同用一個程式。但是由於我們的主程式的程式碼比較複雜,如果讓主程式多開程序的話,會佔用較多的記憶體和CPU,同時觸發不必要的問題。所以專門另寫了一個cef_render專案來作為渲染子程序

cef_render專案程式碼比較簡單,主要程式碼都是繼承CefRenderProcessHandler介面的CefRenderProcessHandler類。考慮到程式碼周全,以後可以在cef_render專案補充一些崩潰Dump處理等程式碼。務必要保證主程式的CefRenderProcessHandler介面實現程式碼與cef_render程式的CefRenderProcessHandler介面實現代一致。否則單程序和多程序模式下會出現不同的處理結果

在瀏覽器程序啟動時,通過附加引數可以指定渲染子程序的路徑

command_line->AppendSwitchWithValue("browser-subprocess-path", "render.exe");
  • 1

C++與JS互動

C++呼叫JS

在browser程序和render程序都可以直接執行JS程式碼,直接呼叫CefFrame物件的ExecuteJavaScript方法就可以

JS呼叫C++

我們專案裡,只需要給JS開放一個函式介面,而且介面並不複雜,所以直接採用JS擴充套件的方法註冊JS回撥函式就可以。

在CefRenderProcessHandler::OnWebKitInitialized接口裡,註冊JS擴充套件程式碼

std::string extensionCode =
    "(function() {"
    "  CefTestWebFunction = function(param) {"
    "    native function CefTestWebFunction(param);"
    "    return CefTestWebFunction(param);"
    "    };"
    "})();";

CefRefPtr<CefV8Handler> handler = new CefJSHandler();
CefRegisterExtension("v8/extern", extensionCode, handler);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

CefRegisterExtension函式會執行擴充套件程式碼。網上例子都是建立一個全域性物件,然後把JS函式和變數繫結到這個物件上。這裡直接申明一個FunExternal的全域性函式。當JS程式碼中呼叫FunExternal函式時,會根據native關鍵字後的函式名,去通知C++程式碼呼叫對應的native函式

CefJSHandler類繼承CefV8Handler介面並實現Execute方法,在CefRegisterExtension傳入CefJSHandle指標,當JS程式碼需要呼叫native函式時會,會主動觸發CefJSHandler::Execute方法

bool CefJSHandler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
    if (name == "CefTestWebFunction" && arguments.size() == 1)
    {
        for (auto &it : arguments)
        {
            if (it->IsString())
            {
                CefString param = it->GetStringValue();

                CefRefPtr<CefBrowser> browser = CefV8Context::GetCurrentContext()->GetBrowser();
                CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kJsCallbackMessage);

                message->GetArgumentList()->SetString(0, name);
                message->GetArgumentList()->SetString(1, param);
                browser->SendProcessMessage(PID_BROWSER, message);

                retval = CefV8Value::CreateBool(true);
            }
        }
        return true;
    }

    // Function does not exist.
    return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

在這裡可以獲取到JS要呼叫的函式名,以及傳入的引數等資訊。獲取到這些資訊後,把他們包裝為CefProcessMessage結構,通過IPC把資訊傳送到Browser程序進行非同步處理。呼叫SendProcessMessage方法把資訊傳送到Browser程序

瀏覽器程序的CefClient::OnProcessMessageReceived方法接收到Render程序發來的訊息。Browser程序處理訊息後,可以通過C++呼叫JS的方法去通知Web端訊息處理結果

程序結束的流程

CefControl控制元件的銷燬流程

  1. 在CefControl控制元件的解構函式裡呼叫CloseBrowser(true)方法通知瀏覽器物件要關閉
  2. BrowserHandler::DoClose介面被觸發,這裡不需要做額外處理,直接返回就可以
  3. 之後BrowserHandler::OnBeforeClose介面被觸發,在這裡一定要釋放所有對CefBrowser物件的引用,否則會導致程式無法退出。

程序退出流程

  1. 使用者單擊右下角托盤的退出選單項
  2. 觸發到LoginCallback::DoLogout函式,在這裡會呼叫到程式碼nim_comp:: WindowsManager::GetInstance()->DestroyAllWindows();,這裡銷燬所有的窗體,所有控制元件被銷燬,自然就會觸發所有CefControl控制元件的銷燬流程,所有瀏覽器物件被關閉
  3. LoginCallback::DoLogout函式裡之後會呼叫到UILogoutCallback函式,這裡原本會呼叫PostQuitMessage(0)函式結束訊息迴圈,但是我們應該等待所有瀏覽器物件關閉後在結束訊息迴圈,否則會發生錯誤。而CefBrowser的關閉是非同步的,所以無法保證呼叫UILogoutCallback函式時所有CefBrowser被關閉
  4. 我在CefManager類實現PostQuitMessage函式,在這裡等待所有CefBrowser關閉後再結束訊息迴圈
  5. 程式正常結束

CefManager::PostQuitMessage函式裡判斷當前瀏覽器物件的數量來決定是否退出訊息迴圈,如果還有瀏覽器物件沒有關閉就等待500毫秒後再檢測:

void CefManager::PostQuitMessage(int nExitCode)
{
    if (browser_count_ == 0)
    {
        Post2UI([nExitCode]()
        {
            ::PostQuitMessage(nExitCode);
        });
    }
    else
    {
        auto cb = [nExitCode]()
        {
            CefManager::GetInstance()->PostQuitMessage(nExitCode);
        };

        nbase::ThreadManager::PostDelayedTask(kThreadUI, cb, nbase::TimeDelta::FromMilliseconds(500));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

把Cef元件整合到雲信NIM專案

  1. tool_kits\cef目錄中寫好的Cef模組元件拷貝到自己專案的對應目錄,並且新增到解決方案中。其中cef_render專案是Cef渲染子程序,是一個獨立的exe;libcef_dll_wrapper專案是Cef匯出的C語言介面的C++包裝類;cef_module專案是核心封裝程式碼,把cef功能封裝為可以在nim demo中直接使用的類,其中包含了對cef功能進行管理的CefManager和CefControl、CefNativeControl兩個控制元件等。
  2. nim_win_demo\gui\cef目錄的原始檔拷貝都自己專案的某個目錄,這裡面CefControl、CefNativeControl兩個控制元件的測試視窗程式碼,可有可無
  3. 進入libs目錄,解壓cef_sandbox.rar壓縮包並把cef_sandbox.lib、cef_sandbox_d.lib檔案放到libs目錄;進入libs\x64目錄,解壓cef_sandbox.rar壓縮包並把cef_sandbox.lib、cef_sandbox_d.lib檔案放到libs\x64目錄。這裡面是編譯cef元件時,為cef模組增加sandbox功能的靜態庫。bin\cef目錄是cef模組依賴的cef相關dll。主程式初始化時會從bin\cef目錄載入cef所需dll
  4. 配置nim_demo專案屬性,在連結器\輸入\延遲載入的DLL中加入libcef.dll;libEGL.dll;libGLESv2.dll(因為我們把cef所需的dll都放到bin\cef目錄了,這樣不會導致目錄混亂,但是為了順利載入cef dll,需要延遲載入;如果不想用延遲載入,就把bin\cef目錄的dll都直接放到bin目錄)
  5. WinMain函式中第一句加入(用於延遲載入cef dll,如果不延遲載入,則不需要這句)
    nim_ui:: InitManager::GetInstance()->AddCefDllToPath();
    在開始雲信元件初始化之前加入如下程式碼用於初始化cef功能(一定要在雲信元件初始化之前)
    if (!nim_cef::CefManager::GetInstance()->Initialize(true)
    return 0;
    在開始UI執行緒訊息迴圈之後加入如下程式碼用於清理cef功能
    nim_ui::InitManager::GetInstance()->CleanupUiKit();

  6. 找到原專案中呼叫::PostQuitMessage函式的地方,修改為nim_cef:: CefManager::GetInstance()->PostQuitMessage(0);

  7. 其他配置如果有疑問可以參見Cef Nim Demo的配置

讓Cef支援Flash、mp3、mp4

現在附帶的nim demo中使用的cef相關dll是專門下載了cef原始碼增加mp3、mp4功能後重新編譯的(在Windows下編譯Cef3.2623並加入mp3、mp4支援(附帶原始碼包和最終DLL) )。所以如果使用我提供的dll,可以直接支援mp3、mp4播放(官方直接下載的cef不支援)。如果對cef功能有其他需求的話請自行下載編譯cef並替換demo中的dll

demo中附帶的dll都是release版本,沒有附帶debug版本

cef_module專案中已經預設支援flash播放,bin\cef\PepperFlash目錄中附帶了支援flash播放所需的dll,如果不需要flash功能,可以刪除這個目錄

cef_module專案中提供了兩個控制元件來展示cef瀏覽器,分別為CefControl、CefNativeControl,CefControl用於離屏渲染模式,CefNativeControl用於真視窗模式,根據需求來選擇使用這兩個控制元件的一個。離屏渲染模式的話控制元件自己控制瀏覽器的渲染,所以可以與nim duilib結合的更完美,支援透明異形窗體;真視窗模式因為Cef需要依託一個子視窗,由Cef自己渲染,所以無法支援透明異形窗體。對於絕大多數需求,使用離屏渲染模式的CefControl更好,因為與duilib結合更完美。但是如果網頁的內容重新整理非常頻繁(尤其是用於播放Flash時),應該使用真視窗模式,否則Flash播放導致的頻繁繪製操作會讓程式的CPU佔用率飆升!

我們的程式碼預設是開啟離屏渲染模式的,如果有播放Flash的需求或者其他瀏覽器畫面頻繁的需求時,應該關閉離屏渲染模式而使用真視窗模式,關於方法時Winmain函式中初始化cef功能時引數傳入falsenim_cef:: CefManager::GetInstance()->Initialize(false)。另外我們的duilib視窗預設是使用支援透明異形的分層視窗,是不支援子視窗的,所以如果使用cef的真視窗模式,那麼應該關閉duilib視窗的分層視窗樣式,關閉方法是建立視窗的Window::Create函式的第五個引數isLayeredWindow設定為false。demo中CefForm、CefNativeForm這兩個窗體類分別用於演示離屏渲染模式(對應CefControl控制元件)和真視窗模式(對應CefNativeControl控制元件)的功能。這兩個視窗的建立程式碼在MainForm::OnClicked中有演示程式碼

禁用Cef模組功能

cef_module專案中預處理巨集中增加了兩個控制元件Cef模組功能的巨集SUPPORT_CEF、SUPPORT_CEF_FLASHSUPPORT_CEF巨集控制是否啟用cef功能,SUPPORT_CEF_FLASH控制cef是否支援flash播放功能(只有SUPPORT_CEF巨集啟用時這個巨集才有效)。

如果不需要cef帶來的瀏覽器功能,可以在cef_module專案中去掉SUPPORT_CEF巨集,這樣cef相關的功能就被禁用。同時*bin\cef*目錄就可以刪除掉而不影響程式執行。

如果需要cef功能但是並不需要flash功能,可以在cef_module專案中去掉SUPPORT_CEF_FLASH巨集。同時bin\cef\PepperFlash**目錄可以刪除掉、**libs目錄的cef_sandbox.lib、cef_sandbox_d.lib檔案也可以刪掉。

在開啟了Flash功能後,要在編譯時加入cef_sandbox.lib等靜態庫,否則在使用flash功能時會有一個黑框彈出(這輸入cef的bug),關於這個bug的其他解決辦法,詳見:解決cef載入flash時彈出黑框的問題。同時程式將無法通過附加引數指定渲染子程序(此時必須用主程序exe來做渲染子程序),這時也就不需要cef_render專案編譯的render.exe了。如果禁用Flash功能,則會讓render.exe來作為渲染子程序

整合過程中遇到的其他坑

通過這些時間用Cef,發現坑其實不少,而且各個版本的坑不一樣。

  1. multi_threaded_message_loop導致中斷:在2623、2526版本,Debug模式中,如果開啟了multi_threaded_message_loop,當程式退出時,必定會觸發中斷。這個屬於Cef的bug,在官方demo中也有這個問題,但是在Release模式中是沒有問題的。
  2. 2357版本在程式處理重定向資訊後,會導致渲染程序崩潰,這個版本無法用於專案
  3. 1916版本各個功能使用正常。但是在在Debug模式下某些網頁開啟時會出中斷警告(但並不是錯誤),可能是因為對新html標準支援不夠;Debug模式下單程序模式在退出時會觸發中斷。但是在Release模式下都正常使用
  4. 設定CefSettings的cache_path欄位(也就是LocalStorage),一定要注意不要在路徑末尾新增”\\”,否則會觸發中斷
  5. 在多程序模式下,必須設定子程序的程式名(不管是使用原程式作為子程序,還是單獨一個程式作為子程序)。當Cef呼叫LoadUrl函式載入網頁時,會查詢子程序的絕對路徑去啟動渲染程序,如果不設定子程序名字,會導致查路徑找發生錯誤,導致VS在Debug模式下卡死
  6. 如果開發者不負責Cef相關功能的開發,可以修改CefManager::AddCefDllToPath函式的程式碼,讓Cef不管在Debug模式還是Release模式下都使用Release版本的Cef Dll檔案。這樣做不會發生錯誤,而且上面提到的多數坑都不會被觸發
  7. 如果在使用Cef模組中遇到一些崩潰或者其他異常現象,請先使用release模式+開啟多程序模式再執行一次,很多問題都是debug模式或者單程序模式導致的
  8. 如果使用flash功能,就需要在編譯時加入cef_sandbox.lib等靜態庫,否則在使用flash功能時會有一個黑框彈出(這輸入cef的bug)。而加入sandbox功能後,在某些電腦上離屏渲染功能就無法順利建立子程序,導致沒有畫面。這時就要在子程序建立前檢查命令列引數,發現不是flash程序就增加no-sandbox引數來關閉sandbox功能
  9. 發現一個非常奇葩的bug,離屏渲染+多執行緒訊息迴圈模式下,在瀏覽器物件上右擊彈出選單,是無法正常關閉的。翻cef原始碼後發現選單是用TrackPopupMenu函式建立的,在MSDN資料上檢視後發現呼叫TrackPopupMenu前需要給其父視窗呼叫SetForegroundWindow,但是在cef原始碼中沒有呼叫。最終翻cef原始碼後得到的解決方法是在cef的UI執行緒建立一個視窗,這個窗體的父視窗必須是在主程式UI執行緒建立的,這樣操作之後就不會出現選單無法關閉的bug了,雖然不知道為什麼但是bug解決了

總結

離屏渲染+異形窗體

CefWindowInfo::SetAsWindowless的transparent引數必須設定為true!
離屏渲染+異形窗體

Flash播放效果

Flash播放效果

Redrain QQ:491646717 2017.4.13

--------------------- 本文來自 Redrain 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/zhuhongshu/article/details/70159672?utm_source=copy