QT集成CEF03-QT简单集成

使用 VS 2019 创建项目,在创建项目之前要先确保已经安装了 QT SDK 和 QT VS 插件。

创建新项目的时候,选择 Qt Widgets Application

在这里插入图片描述
在这里插入图片描述

1. 准备CEF库

上一个步骤中,已经编译好了 libcef_dll_wrapper 库,现在要在新的QT项目中使用这个库,需要做如下的准备。

1.1 在解决方案目录下新建一个libs目录

用来存放第三方的库,目录结构如下:
在这里插入图片描述

  • libs/cef/bin

    这个目录中存放的是 CEF运行的时候所需要的所有依赖文件(如 dll文件),分成了 debug版本和release版本。分别到 cefsimple 项目中进行拷贝。以debug为例,到cef_win32\tests\cefsimple\Debug 中拷贝除了 cefsimple.execefsimple.pdb 这两个文件以外其它所有文件到新建项目的 libs/cef/bin/debug目录中, release 版本类似

  • libs/cef/include

    存放要用到的cef 头文件。cef二进制发行包下有个 include目录,直接将这个目录下所有文件以及子目录拷贝到 libs/cef/include目录中。

  • libs/cef/lib

    存放要用到的 cef 的 *.lib 文件。主要是两个文件:libcef.liblibcef_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

1.2 配置include

项目中需要用到cef 头文件,需要在 vs项目属性中配置 include的路径:
在这里插入图片描述

这里使用宏做了配置:

$(SolutionDir)\libs\cef

宏展开后为:

E:\QyCef\QyCefVS\libs\cef

1.3 配置 运行依赖

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,所有的依赖已经全部拷贝过来了,可执行文件现在就可以直接运行了。
在这里插入图片描述

如果不配置 “生成后事件” ,就需要手工拷贝这些依赖文件。

2. 简单集成

这里的简单集成是像 cefsimple项目一样使用cef自己的消息循环,cef自己创建浏览器窗口,其实与QT没有任何关系。相当于使用 cefsimple项目中的源代码在这个项目中运行。

2.1 拷贝 cefsimple中的源码

在本项目中新建一个cef的文件夹,拷贝cefsimple项目中的4个文件:

  • simple_app.cc
  • simple_app.h
  • simple_handler.cc
  • simple_handler.h

到这个文件夹中:
在这里插入图片描述

红色框中的文件是新建QT 项目的时候生成的,这里除了main.cpp 文件以外,mainwindow 是QT 的窗体,暂时还用不到。
在这里插入图片描述

将这4个文件添加到项目中:
在这里插入图片描述

2.2 修改文件

加入进来的文件编译会报错,这里我们只是简单集成,没有拷贝所有文件,所以需要对这些文件做一些修改后才能正常编译。

  • simple_app.h

    修改后的内容:(忽略了宏定义和一些注释)

    #include "include/cef_app.h"
    
    // Implement application-level callbacks for the browser process.
    class SimpleApp : public CefApp, public CefBrowserProcessHandler {
     public:
      SimpleApp();
      // CefApp methods:
      CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() OVERRIDE {
        return this;
      }
      // CefBrowserProcessHandler methods:
      void OnContextInitialized() OVERRIDE;
     private:
      // Include the default reference counting implementation.
      IMPLEMENT_REFCOUNTING(SimpleApp);
    };
    

    CEF 是多进程的。Chrome 是基于CEF的,Charome在运行的时候,会在windows系统中创建多个进程。每个进行都由命令行启动,在任务管理器中可以看到这些进程是哪个命令启动的,需要做一些设置:

    在这里插入图片描述

在这里插入图片描述

同样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();
    
      // Provide access to the single global instance of this object.
      static SimpleHandler* GetInstance();
    
      virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() 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;
    
     private:
      const bool use_views_;
    
      // List of existing browser windows. Only accessed on the CEF UI thread.
      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;
    }  // namespace
    
    SimpleHandler::SimpleHandler(bool use_views)
        : use_views_(use_views), 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();
        }
    }
    

    编译项目,保证没有任何错误。

2.3入口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[])
{
    // Enable High-DPI support on Windows 7 or newer.
    CefEnableHighDPISupport();
    // 通过GetModuleHandle 获取 HINSTANCE
    HINSTANCE hInstance = GetModuleHandle(nullptr);
    //CEF 命令行参数
    CefMainArgs main_args(hInstance);
    
    // CefExecuteProcess 函数是用来创建进程的。
    // 本程序编译完成后的可执行文件是 QyCefVS.exe 首次启动的时候会创建主进程
    // exit_code是一个负数,可以用QT 的qDebug输出
    // CEF 需要创建子进程的时候,会再次调用 QyCefVS.exe 来启动进程,但是这一次会在调用QyCefVS.exe
    // 的时候传递参数,表明创建的是子进程,比如 “--type=renderer”
    // 这时CefExecuteProcess执行完毕之后,就会返回一个大于0的值,此时就会退出而不会继续向后面执行。
	int exit_code = CefExecuteProcess(main_args, nullptr, nullptr);
    qDebug() << "exit_code:" << exit_code;

    if (exit_code >= 0) {
        // 如果exit_code 大于等于0, 表示创建的是子进程,直接退出了
        return exit_code;
    }
    // CEF 全局设置
    CefSettings settings;
    settings.no_sandbox = true;

  // SimpleApp implements application-level callbacks for the browser process.
  // It will create the first browser instance in OnContextInitialized() after
  // CEF has initialized.
    CefRefPtr<SimpleApp> app(new SimpleApp);

    // 初始化CEF
    CefInitialize(main_args, settings, app.get(), nullptr);

    // CEF 消息循环
    CefRunMessageLoop();

    // Shut down CEF.
    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"
... //其它代码省略

// 新加的函数
// 默认返回的是 false,这里在主frame中加载地址,然后返回true
  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())
      {
          //获取浏览器对象中的 主frame对象,然后加载url
          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>
      <!--The ID below indicates application support for Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
      <!--The ID below indicates application support for Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
      <!--The ID below indicates application support for Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
      <!--The ID below indicates application support for Windows 8.1 -->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
      <!--The ID below indicates application support for Windows 10 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
    </application>
  </compatibility>
</assembly>

将这个文件拷贝到当前项目中:
在这里插入图片描述

然后为项目配置 manifest 条目:
在这里插入图片描述

指定的值为:

$(ProjectDir)compatibility.manifest

$(ProjectDir) 是 当前项目目录的宏,为它指定 manifest文件。只需要为 Release模式指定即可。

再次编译后运行,发现能够正常显示了。
代码请访问 GitHub qt_cef_03分支


版权声明:本文为paopao_wu原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。