QT作為C++下著名的跨平台軟件開發框架,實現了一套代碼可以在所有的操作系統、平台和屏幕類型上部署。我們前幾篇文章講解了如何構建一款基於CEF的簡單的樣例,但這些樣例的GUI都是使用的原生的或者是控件功能不強大的CEF視圖框架。本文將會重新開始,使用VS2019編寫一款基於QT的並嵌入原生窗體的文章。
環境搭建
在本文中,我沒有使用QtCreator進行項目搭建的工作,而是使用VS配合QT VS Tools類來完成項目的環境。在本文,假設你已經安裝了QT,並且了解QT的相關知識。
安裝Qt VS Tools插件
在VS中,我們通過在擴展(Extension)搜索對應的QT插件,完成安裝工作,安裝完成后,需要重啟VS。
配置Qt環境
找到Extensions - Qt VS Tools - Options
:
找到Qt - Versions
,進行QT - VS編譯的配置:
Qt項目創建
在經過配置以后,此時使用VS進行項目創建的時候,會發現創建的向導頁面會出現Qt的相關項目模板:
接下來創建一個名為QtCefDemo的樣例,此時會彈出Qt的創建向導:
然后,Qt會自動幫我們配置好Debug和Release:
最后,我們再調整下項目的文件:
點擊Finish
,我么就得到了如下的在VS IDE下的QT項目大致結構:
當我們運行該項目以后,就可以看到目前的一個簡單的QT窗體:
當然,本文的目的不僅僅是創建一個Qt窗體那樣的簡單,還需要進行CEF的簡單集成。所以,接下來我們繼續配置CEF的環境。
配置CEF環境
在前一篇文章,我們已經了解如何編譯libcef_dll_wrapper
這個庫,所以,本文假設你已經編譯出了libcef_dll_wrapper.lib(Debug和Release版本,並且對應版本的程序集類型分別是:MDd和MD):
接下來,我們需要在我們的解決方案下,創建對應的文件夾,用來存放CEF在編譯和運行時會使用到的頭文件、庫文件以及資源文件。
拷貝頭文件以及資源文件
首先,我們在解決方案同級目錄下創建一個名為CefFiles
的文件夾,將cef文件中的Release和Include拷貝進來:
拷貝二進制庫文件
接下來,我們在CefFiles文件夾中創建一個bin
目錄,用於存放libcef.lib相關文件以及ibcef_dll_wrapper.lib庫文件,但需要注意的是,我們需要按照Debug和Release進行分類:
對於拷貝libcef_dll_wrapper.lib文件,我們也拷貝到對應的bin/版本目錄下:
Release的同理:
此時,我們的CefFiles文件結構如下:
CefFiles
├─bin
│ ├─Debug
│ │ │ ...
│ │ │ libcef.dll
│ │ │ libcef.lib
│ │ │ libcef_dll_wrapper.lib
│ │ │ ...
│ │ │
│ │ └─swiftshader
│ │ ...
│ │
│ └─Release
│ │ ...
│ │ libcef.dll
│ │ libcef.lib
│ │ libcef_dll_wrapper.lib
│ │ ...
│ │
│ └─swiftshader
│ ...
│
├─include
│ 各種.h頭文件
│ ...
└─Resources
│ cef.pak
│ ..
└─locales
...
zh-CN.pak
zh-TW.pak
編寫manifest文件
在Windows上使用CEF的時候,需要配置將manifest文件打入exe可執行程序中,這個manifest文件我們直接手工創建,在項目目錄下創建一個名為app.manifest
的文件:
內容如下:
<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!--The ID below indicates application support for Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- 10.0 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
配置VS中的頭文件以及庫文件的加載地址
首先是配置頭文件include的目錄:
由於頭文件不存在Debug和Release的差別,所以Release相同配置,不再贅述。
接下來是配置鏈接庫的文件路徑,由於Debug和Release下,庫文件內容存在不同,所以需要分別配置,但我們看可以使用$(Configuration)宏
來完成根據環境自動配置。
在Release下,只需要同樣的配置,但是會自動定位。
配置manifest文件
當然,由於manifest文件不涉及Debug還是Release,所以配置一致即可。
至此,我們的使用VS作為IDE,基於QT的框架的,集成CEF的環境完全搭建完成了,在文章的末尾,我會附上在環境搭建完成下的初始狀態的項目。
集成CEF的編碼
在CEF編碼的時候,我們直接將cefsimple中的相關代碼遷移到我們的項目中,但是會進行一定的刪改。
編寫simple_handler
simple_handler.h
#pragma once
#include "include/cef_client.h"
#include <list>
class SimpleHandler : public CefClient,
public CefLifeSpanHandler,
public CefLoadHandler
{
public:
explicit SimpleHandler();
~SimpleHandler();
// Provide access to the single global instance of this object.
static SimpleHandler* GetInstance();
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE
{
return this;
}
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() 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;
// CefLoadHandler methods:
virtual void OnLoadError(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
ErrorCode errorCode,
const CefString& errorText,
const CefString& failedUrl) OVERRIDE;
// Request that all existing browser windows close.
void CloseAllBrowsers(bool force_close);
bool IsClosing() const { return is_closing_; }
private:
// List of existing browser windows. Only accessed on the CEF UI thread.
typedef std::list<CefRefPtr<CefBrowser>> BrowserList;
BrowserList browser_list_;
bool is_closing_;
// Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(SimpleHandler);
};
simple_handler.cpp
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#include "simple_handler.h"
#include <sstream>
#include <string>
#include "include/base/cef_bind.h"
#include "include/cef_app.h"
#include "include/cef_parser.h"
#include "include/views/cef_browser_view.h"
#include "include/views/cef_window.h"
#include "include/wrapper/cef_closure_task.h"
#include "include/wrapper/cef_helpers.h"
namespace
{
SimpleHandler* g_instance = nullptr;
// Returns a data: URI with the specified contents.
std::string GetDataURI(const std::string& data, const std::string& mime_type)
{
return "data:" + mime_type + ";base64," +
CefURIEncode(CefBase64Encode(data.data(), data.size()), false)
.ToString();
}
} // namespace
SimpleHandler::SimpleHandler(): is_closing_(false)
{
DCHECK(!g_instance);
g_instance = this;
}
SimpleHandler::~SimpleHandler()
{
g_instance = nullptr;
}
// static
SimpleHandler* SimpleHandler::GetInstance()
{
return g_instance;
}
void SimpleHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser)
{
CEF_REQUIRE_UI_THREAD();
// Add to the list of existing browsers.
browser_list_.push_back(browser);
}
bool SimpleHandler::DoClose(CefRefPtr<CefBrowser> browser)
{
CEF_REQUIRE_UI_THREAD();
// Closing the main window requires special handling. See the DoClose()
// documentation in the CEF header for a detailed destription of this
// process.
if (browser_list_.size() == 1)
{
// Set a flag to indicate that the window close should be allowed.
is_closing_ = true;
}
// Allow the close. For windowed browsers this will result in the OS close
// event being sent.
return false;
}
void SimpleHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser)
{
CEF_REQUIRE_UI_THREAD();
// Remove from the list of existing browsers.
BrowserList::iterator bit = browser_list_.begin();
for (; bit != browser_list_.end(); ++bit)
{
if ((*bit)->IsSame(browser))
{
browser_list_.erase(bit);
break;
}
}
if (browser_list_.empty())
{
// All browser windows have closed. Quit the application message loop.
CefQuitMessageLoop();
}
}
void SimpleHandler::OnLoadError(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
ErrorCode errorCode,
const CefString& errorText,
const CefString& failedUrl)
{
CEF_REQUIRE_UI_THREAD();
// Don't display an error for downloaded files.
if (errorCode == ERR_ABORTED)
return;
// Display a load error message using a data: URI.
std::stringstream ss;
ss << "<html><body bgcolor=\"white\">"
"<h2>Failed to load URL "
<< std::string(failedUrl) << " with error " << std::string(errorText)
<< " (" << errorCode << ").</h2></body></html>";
frame->LoadURL(GetDataURI(ss.str(), "text/html"));
}
void SimpleHandler::CloseAllBrowsers(bool force_close)
{
if (!CefCurrentlyOn(TID_UI))
{
// Execute on the UI thread.
CefPostTask(TID_UI, base::Bind(&SimpleHandler::CloseAllBrowsers, this,
force_close));
return;
}
if (browser_list_.empty())
return;
BrowserList::const_iterator it = browser_list_.begin();
for (; it != browser_list_.end(); ++it)
(*it)->GetHost()->CloseBrowser(force_close);
}
編寫simple_app
simple_app.h
#pragma once
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#include "include/cef_app.h"
// Implement application-level callbacks for the browser process.
class SimpleApp : public CefApp, public CefBrowserProcessHandler
{
public:
SimpleApp();
// CefApp methods:
virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler()
OVERRIDE
{
return this;
}
// CefBrowserProcessHandler methods:
virtual void OnContextInitialized() OVERRIDE;
private:
// Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(SimpleApp);
};
simple_app.cpp
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
#include "simple_app.h"
#include <string>
#include "include/views/cef_window.h"
#include "include/wrapper/cef_helpers.h"
#include "simple_handler.h"
SimpleApp::SimpleApp()
{
}
void SimpleApp::OnContextInitialized()
{
CEF_REQUIRE_UI_THREAD();
}
編寫入口代碼處理函數集成CEF
main.cpp
對於入口函數,目前只是進行QT相關代碼的編寫,我們還需要對CEF進行初始化操作,對於該文件整體如下:
#include <cef_app.h>
#include "qtcefwindow.h"
#include "stdafx.h"
#include <QtWidgets/QApplication>
#include "simple_app.h"
/**
* 初始化QT以及CEF相關
*/
int init_qt_cef(int& argc, char** argv)
{
const HINSTANCE h_instance = static_cast<HINSTANCE>(GetModuleHandle(nullptr));
const CefMainArgs main_args(h_instance);
const CefRefPtr<SimpleApp> app(new SimpleApp); //CefApp實現,用於處理進程相關的回調。
const int exit_code = CefExecuteProcess(main_args, app.get(), nullptr);
if (exit_code >= 0)
{
return exit_code;
}
// 設置配置
CefSettings settings;
settings.multi_threaded_message_loop = true; //多線程消息循環
settings.log_severity = LOGSEVERITY_DISABLE; //日志
settings.no_sandbox = true; //沙盒
CefInitialize(main_args, settings, app, nullptr);
return -1;
}
int main(int argc, char* argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // 解決高DPI下,界面比例問題
QApplication a(argc, argv);
const int result = init_qt_cef(argc, argv);
if (result >= 0)
{
return result;
}
QtCefWindow w;
w.show();
a.exec();
CefShutdown(); // 關閉CEF,釋放資源
return 0;
}
修改qtcefwindow窗體代碼
qtcefwindow.h
為窗體添加私有成員:CefRefPtr<SimpleHandler>
#pragma once
#include <QtWidgets/QMainWindow>
#include "simple_handler.h"
#include "ui_qtcefwindow.h"
class QtCefWindow : public QMainWindow
{
Q_OBJECT
public:
QtCefWindow(QWidget *parent = Q_NULLPTR);
private:
Ui::QtCefWindowClass ui;
CefRefPtr<SimpleHandler> simple_handler_; // 這里是新增的CefRefPtr<SimpleHandler>成員
};
qtcefwindow.cpp
在構造函數中,處理關聯qtcefwindow和SimpleHandler:
#include "qtcefwindow.h"
#include <cef_request_context.h>
#include "simple_handler.h"
#include "stdafx.h"
QtCefWindow::QtCefWindow(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
// 以下是將 SimpleHandler 與窗體進行關聯的代碼
CefWindowInfo cef_wnd_info;
QString str_url = "https://www.cnblogs.com/w4ngzhen";
RECT win_rect;
QRect rect = this->geometry();
win_rect.left = rect.left();
win_rect.right = rect.right();
win_rect.top = rect.top();
win_rect.bottom = rect.bottom();
cef_wnd_info.SetAsChild((HWND)this->winId(), win_rect); //將cef界面嵌入qt界面中
CefBrowserSettings cef_browser_settings;
simple_handler_ = CefRefPtr<SimpleHandler>(new SimpleHandler());
CefBrowserHost::CreateBrowser(cef_wnd_info,
simple_handler_,
str_url.toStdString(),
cef_browser_settings,
nullptr,
CefRequestContext::GetGlobalContext());
}
運行代碼
終於,項目搭建完成以后,我們走到了最后一步,看看我們在Qt中集成CEF的效果吧。
運行程序,會發現報錯:
---------------------------
QtCefDemo.exe - 系統錯誤
---------------------------
由於找不到 libcef.dll,無法繼續執行代碼。重新安裝程序可能會解決此問題。
---------------------------
確定
---------------------------
對於這個問題,其實我們就是缺少運行時候的相關庫文件,這里我們暫時先手動進行拷貝工作,以Debug環境為例,我們將資源文件拷貝到輸出目錄中:
然后將CefFiles\bin\Debug
中所有的文件拷貝到輸出目錄中:
當然,我們可以通過配置自動化腳本的方式,讓IDE幫助我們拷貝這些文件,但本文不討論這個問題。在手動拷貝了文件以后,我們再次嘗試運行。
終於,我們看到了我們想要的頁面,不過似乎渲染顯示還有點問題,不過在本文我們暫且不討論。在后續,我會單獨寫一篇文章,來談一談使用CEF以及QT集成CEF的過程中會遇到的各種問題以及解決方案。
附錄:源碼以及相關文件
本文所涉及的項目源碼在:w4ngzhen/QtCefDemo (github.com)
其中,會有兩個提交:
- project init
- integrate CEF code
讀者可以自行創建分支,回退到指定的提交查看對應狀態的代碼。
此外,本Demo還需要我們創建的CefFiles文件夾以及其中的文件。由於Github對於大文件的處理不太方便。本人將其上傳到了網盤,讀者只需要從網盤下載CefFiles.zip文件,並將其解壓到解決方案同級目錄即可。
網盤地址:鏈接:https://pan.baidu.com/s/1BylLcETsFAJ5-TnmzpRxeA
提取碼:bydn