使用 VS 2019 创建项目,在创建项目之前要先确保已经安装了 QT SDK 和 QT VS 插件。
创建新项目的时候,选择 Qt Widgets Application
准备CEF库
上一个步骤中,已经编译好了 libcef_dll_wrapper 库,现在要在新的QT项目中使用这个库,需要做如下的准备。
-
在解决方案目录下新建一个libs目录,用来存放第三方的库,目录结构如下:
-
libs/cef/bin 这个目录中存放的是 CEF运行的时候所需要的所有依赖文件(如 dll文件),分成了 debug版本和release版本。分别到 cefsimple 项目中进行拷贝。以debug为例,到cef_win32\tests\cefsimple\Debug 中拷贝除了 cefsimple.exe 和 cefsimple.pdb 这两个文件以外其它所有文件到新建项目的 libs/cef/bin/debug目录中, release 版本类似 -
libs/cef/include 存放要用到的cef 头文件。cef二进制发行包下有个 include目录,直接将这个目录下所有文件以及子目录拷贝到 libs/cef/include目录中。 -
libs/cef/lib 存放要用到的 cef 的 *.lib 文件。主要是两个文件:libcef.lib 和 libcef_dll_wrapper.lib ,它们 也分 debug版本和 release 版本。以 debug版本为例,libcef_dll_wrapper.lib 文件: CMake 生成的vs 项目/libcef_dll_wrapper/Debug/libcef_dll_wrapper.lib , libcef.lib 文件: cef二进制发行包/Debug/libcef.lib -
配置include 项目中需要用到cef 头文件,需要在 vs项目属性中配置 include的路径: 这里使用宏做了配置: $(SolutionDir)\libs\cef
宏展开后为: E:\QyCef\QyCefVS\libs\cef
-
配置 运行依赖 QyCefVS 项目编译以后,以Debug为例,可执行文件默认生成到 解决方案目录/Debug目录中了,但是这个可执行的exe文件需要cef和 QT 依赖才能运行,而这些依赖文件并没有在这个目录中。需要手动去拷贝。 cef依赖很简单,直接拷贝 libs/cef/bin/debug目录下的所有文件即可。 QT依赖需要通过 windeployqt.exe 这个辅助工具来生成,windeployqt.exe 完整路径: D:\Qt\5.15.2\msvc2019\bin\ windeployqt.exe 为了方便,也可以在 QyCefVS项目 中配置 “生成后事件”,VS 通过运行命令来完成自动的拷贝。 这里还是以 Debug为例,我的配置是: XCOPY $(SolutionDir)libs\cef\bin\debug\* $(OutputPath) /s /e /y
$(QtDllPath)\windeployqt.exe $(OutputPath)$(TargetFileName)
这里用到了一些宏定义,其实就是一些变量,计算后的值是: XCOPY E:\QyCef\QyCefVS\libs\cef\bin\debug\* E:\QyCef\QyCefVS\Debug\ /s /e /y
D:/Qt/5.15.2/msvc2019/bin\windeployqt.exe E:\QyCef\QyCefVS\Debug\QyCefVS.exe
项目编译以后,查看解决方案下Debug,所有的依赖已经全部拷贝过来了,可执行文件现在就可以直接运行了。
如果不配置 “生成后事件” ,就需要手工拷贝这些依赖文件。
简单集成
这里的简单集成是像 cefsimple项目一样使用cef自己的消息循环,cef自己创建浏览器窗口,其实与QT没有任何关系。相当于使用 cefsimple项目中的源代码在这个项目中运行。
拷贝 cefsimple中的源码
在本项目中新建一个cef的文件夹,拷贝cefsimple项目中的4个文件:
- simple_app.cc
- simple_app.h
- simple_handler.cc
- simple_handler.h
到这个文件夹中:
红色框中的文件是新建QT 项目的时候生成的,这里除了main.cpp 文件以外,mainwindow 是QT 的窗体,暂时还用不到。
将这4个文件添加到项目中:
修改文件
加入进来的文件编译会报错,这里我们只是简单集成,没有拷贝所有文件,所以需要对这些文件做一些修改后才能正常编译。
同样CEF也会用命令行启动进程,主要包含 浏览器进程 和 渲染进程 ,那么这些进程启动后,我们的程序要执行的代码怎么才能“注入” 到 CEF框架中,让框架来回调 , 这就需要通过 CefApp 这个接口了。
SimpleApp 实现了CefApp 这个接口,为了简单说明问题,这里只实现了CefApp 中的其中一个接口:
-
GetBrowserProcessHandler() CEF框架说: 我在创建了浏览器进程 之后,你的应用程序需要给我一个对象(CefBrowserProcessHandler),而这个对象由你的应用来实现, 这样CEF框架就知道下一步怎么做了。CEF框架是通过 调用回调 CefApp的 GetBrowserProcessHandler()来获取到的。 而 CefBrowserProcessHandler 也是一个接口,为了方便,就让SimpleApp 顺便也实现了这个接口(C++ 支持多重继承)OnContextInitialized() 就是 CefBrowserProcessHandler 接口中定义的方法,表示"在CEF上下文初始化完成以后,需要做的事情" GetBrowserProcessHandler() 这个方法直接返回了 this, 因为SimpleApp 实现了CefBrowserProcessHandler 这个接口。 -
simple_app.cc 这个文件中修改了一些头文件引用的位置,为了更好的分析CEF框架,删除了其它暂时不用的内容。 #include "simple_app.h"
#include <string>
#include "include/cef_browser.h"
#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();
CefRefPtr<SimpleHandler> handler(new SimpleHandler(false));
CefBrowserSettings browser_settings;
std::string url= "https://www.baidu.com";
CefWindowInfo window_info;
window_info.SetAsPopup(NULL, "cefsimple");
CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings,
nullptr, nullptr);
}
可以看到,在CEF上下文初始化完成以后,调用了 OnContextInitialized 函数,这个函数要完成的功能其实就是创建一个浏览器窗口,由CefBrowserHost::CreateBrowser 函数来完成浏览器窗口的创建。 那浏览器窗口被创建后,会到指定的url地址加载内容,以及后续的一系列都如何来处理,需要为它“注入”一个 CefClient 对象 (CefBrowserHost::CreateBrowser 函数的第二个参数),这个对象中同样定义了大量的框架回调方法,由我们来实现。SimpleHandler 这个类就实现了 CefClient 对象。 -
simple_handler.h 这个头文件中定义了 SimpleHandler 类,它实现了 CefClient 接口。CefClient接口中要求的方法都是要为 CEF框架提供一些 handler对象,主要有:
- CefContextMenuHandler,主要用于处理 Context Menu 事件。
- CefDialogHandler,主要用来处理对话框事件。
- CefDisplayHandler,处理与页面状态相关的事件,如页面加载情况的变化,地址栏变化,标题变化等事件。
- GetDragHandler,处理拖拽相关的事件,如从外边拖入浏览器事件
- CefDownloadHandler,主要用来处理文件下载。
- CefFocusHandler,主要用来处理焦点事件。
- CefGeolocationHandler,用于申请 geolocation 权限。
- CefJSDialogHandler,主要用来处理 JS 对话框事件。
- CefKeyboardHandler,主要用来处理键盘输入事件。
- CefLifeSpanHandler,主要用来处理与浏览器生命周期相关的事件,与浏览器对象的创建、销毁以及弹出框的管理。
- CefLoadHandler,主要用来处理浏览器页面加载状态的变化,如页面加载开始,完成,出错等。
- CefRenderHandler,主要用来处在在窗口渲染功能被关闭的情况下的事件。
- CefRequestHandler,主要用来处理与浏览器请求相关的的事件,如资源的的加载,重定向等。
为了梳理CEF的主流程,这个头文件中并没有返回所有的Handler,只返回了CefLifeSpanHandler,它是为了保证关闭窗口后,程序能正常退出。 其余的大部分代码都被删除了。 #include "include/cef_client.h"
#include <list>
class SimpleHandler : public CefClient,
public CefLifeSpanHandler {
public:
explicit SimpleHandler(bool use_views);
~SimpleHandler();
static SimpleHandler* GetInstance();
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE {
return this;
}
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
virtual bool DoClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
private:
const bool use_views_;
typedef std::list<CefRefPtr<CefBrowser>> BrowserList;
BrowserList browser_list_;
bool is_closing_;
IMPLEMENT_REFCOUNTING(SimpleHandler);
};
SimpleHandler 主要目的是为了给框架提供 CefClient中定义的hander,它同时实现了CefLifeSpanHandler,并重写了CefClient 中的GetLifeSpanHandler() 方法将自己返回,因为它实现了CefLifeSpanHandler。 -
simple_handler.cc #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;
}
SimpleHandler::SimpleHandler(bool use_views)
: use_views_(use_views), is_closing_(false) {
DCHECK(!g_instance);
g_instance = this;
}
SimpleHandler::~SimpleHandler() {
g_instance = nullptr;
}
SimpleHandler* SimpleHandler::GetInstance() {
return g_instance;
}
void SimpleHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
browser_list_.push_back(browser);
}
bool SimpleHandler::DoClose(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
if (browser_list_.size() == 1) {
is_closing_ = true;
}
return false;
}
void SimpleHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
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()) {
CefQuitMessageLoop();
}
}
编译项目,保证没有任何错误。
入口main方法
打开 main.cpp 文件,它是入口函数,程序启动从这里开始。删除建立QT项目时 main方法中生成的代码,为了理清楚CEF框架的集成过程,先不使用QT窗体,而使用 CEF自己创建的窗体和它的消息循环。
#include "mainwindow.h"
#include <QtWidgets/QApplication>
#include "include/cef_command_line.h"
#include "include/cef_sandbox_win.h"
#include "cef/simple_app.h"
#include <qdebug.h>
int main(int argc, char *argv[])
{
CefEnableHighDPISupport();
HINSTANCE hInstance = GetModuleHandle(nullptr);
CefMainArgs main_args(hInstance);
int exit_code = CefExecuteProcess(main_args, nullptr, nullptr);
qDebug() << "exit_code:" << exit_code;
if (exit_code >= 0) {
return exit_code;
}
CefSettings settings;
settings.no_sandbox = true;
CefRefPtr<SimpleApp> app(new SimpleApp);
CefInitialize(main_args, settings, app.get(), nullptr);
CefRunMessageLoop();
CefShutdown();
return 0;
}
注意: 在VS 中使用 QDebug() 是向控制台中输出信息,但是程序启动后,并不会看到输出,需要在项目中配置一下:将原来的 /SUBSYSTEM:WINDOWS 更改为 /SUBSYSTEM:CONSOLE
编译运行项目:
查看程序进程:
此时可以看到启动了多个进程,一个主进程(10832),其它的为子进程,可以看到启动子进程的时候,有 --type参数:
- –type=gpu-process
- –type=utility
- –type= renderer (两个)
问题1: 每次点击链接都在新窗口中打开
程序运行后每次点击链接,都会弹出一个新的窗体,我们需要的是在同一个窗体中打开,如何达到这个目的?
SimpleHandler实现了 CefLifeSpanHandler 接口,这个接口主要用来处理与浏览器生命周期相关的事件。
SimpleHandler 中只重写了 OnAfterCreated,DoClose,OnBeforeClose 这三个方法。我们打开CefLifeSpanHandler的头文件,会看到:
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,
CefRefPtr<CefDictionaryValue>& extra_info,
bool* no_javascript_access) {
return false;
}
OnBeforePopup 这个方法,当该函数返回 false 的时候,则允许弹出窗口,为 true 的时候就拦截掉不允许弹出了。所以我们要重写这个方法,让这个方法返回true,但是如果只是返回true的话,你点击页面上的任何链接都不管用了。
现在看看这个方法的参数:
-
browser 和 frame 分别代表当前浏览器实例和表示了在哪个 frame 触发的这个事件.
注意:每个browser对象中会包含多个frame,比如一个浏览器窗口加载了一个网页,那么这个browser就会有一个主frame, 对应JavaScript中的Frame,每个Frame中都有JavaScript 的window对象。而如果这个网页中使用了 iframe内嵌了一个网页,那么这个browser对象中又会多一个子frame
-
target_url 和 target_frame_name 代表了目标要打开的地址和 frame 名称 -
target_disposition 描述了是从当前页还是从新标签中打开链接 -
user_gesture 如果用户手动点击 a 标签触发这个事件则该属性为 true,否则如果是自动触发的为 false(重要) -
popupFeatures 包含了一些弹窗的信息,是一个结构体自己可以跟进去看一下 -
windowInfo 窗口的信息 -
client 当前客户端实例 -
settings 弹出窗口的设置信息 -
no_javascript_access 是否允许弹出的窗口使用 JS 脚本,如果为 false 则不允许使用并且与当前页面可能不在一个 render 进程中
了解了参数以后,我们可以这样简单粗暴的来解决:就是所有都是在当前窗口打开。实际上还是需要根据实际情况来处理的,比如 HTML的 A标签上明确指明了 target = _blank , 这是就根据情况来做对应的处理。这里为了简单就都统一在当前窗口打开。
在 simple_handler.h 文件中添加一个这个方法,并重写它:
#include "include/wrapper/cef_helpers.h"
...
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,
CefRefPtr<CefDictionaryValue>& extra_info,
bool* no_javascript_access) {
CEF_REQUIRE_UI_THREAD();
if (!target_url.empty())
{
browser->GetMainFrame()->LoadURL(target_url);
return true;
}
return false;
}
重新编译执行后,就会在同一个窗口打开了。
问题2:Release模式窗口空白
我们采用Release模式后运行程序,发现打开的窗口是个空白窗口。
打开运行目录下的 debug.log 文件,会发现:
dwrite_font_proxy_init_impl_win.cc(90)] Check failed: fallback_available == base::win::GetVersion() > base::win::Version::WIN8 (1 vs. 0)
这个错误表示应用程序需要一个带有相关兼容性条目的manifest 文件。
我们到 cef二进制发行包的 tests\cefsimple 文件夹下会找到一个 compatibility.manifest 文件,其内容为(用文本编辑器就可以打开):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
将这个文件拷贝到当前项目中:
然后为项目配置 manifest 条目:
指定的值为:
$(ProjectDir)compatibility.manifest
$(ProjectDir) 是 当前项目目录的宏,为它指定 manifest文件。只需要为 Release模式指定即可。
再次编译后运行,发现能够正常显示了。
|