QT集成CEF05-嵌入到QT窗体

回顾一下CEF 的浏览器窗口是如何创建出来的:

  • SimpleApp实现了CefBrowserProcessHandler 接口

  • CefBrowserProcessHandler 接口中有一个OnContextInitialized 回调,它在CEF创建 context 初始化完毕后被回调。

  • SimpleApp 实现了OnContextInitialized 回调,代码如下:

    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");
        // Create the first browser window.
        CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings,
                                      nullptr, nullptr);
    }
    

    可以看到这个窗口是通过CefBrowserHost::CreateBrowser 创建出来的,创建的时候需要传递以下一个参数:

    • const CefWindowInfo& windowInfo :CefWindowInfo 对象,即窗口信息
    • CefRefPtr<CefClient> client:CefClient 对象。我们自己实现了 SimpleHandler类,它从CefClient继承
    • const CefString& url : 网址url
    • const CefBrowserSettings& settings : 浏览器设置
    • CefRefPtr<CefDictionaryValue> extra_info :额外信息,这里专递了NULL,
    • CefRefPtr<CefRequestContext> request_context : 请求上下文,这里也传递了NULL

    CefWindowInfo 的SetAsPopup 方法让这个窗口弹出来,它有两个参数,第一个参数为父Windows窗口句柄,传递了NULL,因为在前面的例子中,这个窗口是个顶层窗口,第二个参数为窗口标题。

CefWindowInfo

它表示浏览器窗口信息,有三个方法:

  • void SetAsChild(CefWindowHandle parent, RECT windowRect) 让浏览器窗口成为 parent 的内嵌子窗口。

  • void SetAsPopup(CefWindowHandle parent, const CefString& windowName) 让浏览器窗口是个弹出窗口,可以指定父窗口句柄,但是它是一个单独的弹出窗口

  • void SetAsWindowless(CefWindowHandle parent) 浏览器窗口是一个离屏渲染(windowless(off-screen) rendering)窗口。 可以理解成一个内存中存在的浏览器窗口,没有在屏幕上显示。比如做一个爬虫应用或者测试工具的时候需要拿到浏览器的中的内容,但是又没有必要在屏幕上显示出来的场景。

嵌入到QT窗体的思路

有了前面的分析,我们就有了嵌入到QT窗体的大概思路: 当CEF context初始化完毕的时候,获取QT 窗体的句柄,交给CefWindowInfo ,然后再将 CefWindowInfo 交给CefBrowserHost::CreateBrowser 将浏览器窗口创建出来。CefWindowInfo 调用 SetAsChild 方法让浏览器窗口嵌入到 QT窗体。

QT的 QWidget 中有 winId() 方法可以获取窗体句柄。

MainWindow 是从QMainWindow继承的,它里面有个centralWidget,我们可以将这个centralWidget的句柄交给CefWindowInfo

思考一个问题:

main.cpp中 settings.multi_threaded_message_loop = true; 这个配置导致CEF在单独的线程上运行Browser的界面,而不是在主线程上。所以当 SimpleApp::OnContextInitialized 执行的时候,是在单独的线程中。但是QT窗体 MainWindow是在主UI线程中执行的。如果此时 在 OnContextInitialized中执行创建浏览器窗口,并将窗口嵌入到MainWindow中,即在子线程中操作QT UI主线,此时就会有问题:

在QT中,子线程是无法直接操作UI主线程的,运行的时候会报错

此时就可以使用QT的信号槽机制,让子线程给UI 所在线程发出信号,然后在MainWindow 的槽函数中来创建浏览器窗体,并完成嵌入到 centralWidget中的工作。

步骤

下面对SimpleApp, MainWindow, main函数进行改造,完成浏览器嵌入到 QT 主窗体。

SimpleApp 加入信号

  • 要让simpleApp支持QT信号槽机制,那么SimpleApp就要从 QObject 继承,并加入 Q_OBJECT 宏。

  • 在simple_app.h 头文件中定义信号

  • 在OnContextInitialized 中发射信号

继承的时候,要将 QObject 写在最前面,并且是 public继承

// simple_app.h 文件
#include "include/cef_app.h"
#include "QObject"
// 01: 加入 public QObject父类
class SimpleApp : public QObject, public CefApp, public CefBrowserProcessHandler {
    // 02: 因为要支持 信号槽,所以要加入 Q_OBJECT宏
    Q_OBJECT
 public:
  SimpleApp();
  CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() OVERRIDE {
    return this;
  }
  void OnContextInitialized() OVERRIDE;
// 03: 加入onCefOnctextInitialized信号
signals:
    void onCefOnctextInitialized();

 private:
  IMPLEMENT_REFCOUNTING(SimpleApp);
};

// simple_app.cc 文件
#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()
{
}
// 04: 在 OnContextInitialized 中发射onCefOnctextInitialized信号
void SimpleApp::OnContextInitialized() {
  CEF_REQUIRE_UI_THREAD();
  // 发出信号
  emit onCefOnctextInitialized();
}

MainWindow 中连接信号

  • 因为要在 MainWindow中连接SimpleApp中定义的信号,所以要在 mainwindow.h 中定义SimpleApp* m_cefApp 成员用来保存 SimpleApp 对象。
  • 在 MainWindow 构造函数中对 m_cefApp 进行初始化
  • MainWindow 定义槽函数 createBrowserWindow() ,并在 mainwindow.cpp中实现 浏览器窗口的创建,并嵌入到MainWindow 中的centralWidget
  • 在MainWindow 构造函数中将 槽函数 createBrowserWindow()SimpleApp 中的 onCefOnctextInitialized 信号连接起来
// mainwindow.h  文件
#include <QtWidgets/QMainWindow>
#include "ui_mainwindow.h"
#include "cef/simple_app.h"
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    // 01 传入SimpleApp ,用于连接到它的onCefOnctextInitialized 信号
    MainWindow(SimpleApp* cefApp,QWidget* parent = Q_NULLPTR);
// 02 槽函数,完成浏览器窗口的创建与嵌入
private slots:
    void createBrowserWindow();
private:
     SimpleApp*  m_cefApp=NULL;
    Ui::MainWindowClass ui;
};

// mainwindow.cpp 文件
#include "mainwindow.h"
#include "cef/simple_handler.h"
MainWindow::MainWindow(SimpleApp* cefApp,QWidget *parent)
    : QMainWindow(parent),m_cefApp(cefApp)
{
    ui.setupUi(this);
    // 当SimpleApp 中回调OnctextInitialized的时候,通知 主窗体创建浏览器窗口,并嵌入到主窗口中
    connect(m_cefApp, &SimpleApp::onCefOnctextInitialized, this, &MainWindow::createBrowserWindow);
}

/// <summary>
/// 创建浏览器窗体 槽函数
/// </summary>
void MainWindow::createBrowserWindow() {
    CefRefPtr<SimpleHandler> handler(new SimpleHandler(false));
  // 浏览器配置,
  CefBrowserSettings browser_settings;
  // 要打开的网址
  std::string url= "https://www.baidu.com";
  // 浏览器窗口信息
  CefWindowInfo window_info;
  
  //window_info.SetAsPopup(NULL, "cefsimple");
  // 获取嵌入窗体的句柄
  HWND wnd = (HWND)this->centralWidget()->winId();
  CefWindowInfo cefWndInfo;
  RECT winRect;
  QRect qtRect = this->rect();
  winRect.left = qtRect.left();
  winRect.top = qtRect.top();
  winRect.right = qtRect.right();
  winRect.bottom = qtRect.bottom();

  window_info.SetAsChild(wnd, winRect);
    // Create the first browser window.
    CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings,
                                  nullptr, nullptr);
}

main.cpp 入口

在创建 MainWindow 对象之前先创建 SimpleApp 对象,然后传入 MainWindow 构造方法中

int main(int argc, char *argv[])
{
    CefEnableHighDPISupport();
    HINSTANCE hInstance = GetModuleHandle(nullptr);
    CefMainArgs main_args(hInstance);
	int exit_code = CefExecuteProcess(main_args, nullptr, nullptr);
    if (exit_code >= 0) {
        return exit_code;
    }
    CefSettings settings;
    settings.no_sandbox = true;
    // 这个设置项将导致CEF在单独的线程上运行Browser的界面,而不是在主线程上。
    settings.multi_threaded_message_loop = true;
    // 创建 SimpleApp 对象
    SimpleApp* cefApp=new SimpleApp;
    QApplication a(argc, argv);
    // 为 MainWindow构造方法中传入 SimpleApp
    MainWindow w(cefApp, nullptr);
    w.show();
    
    CefRefPtr<SimpleApp> app(cefApp);
    CefInitialize(main_args, settings, app.get(), nullptr);
    int ret = a.exec();
    // Shut down CEF.
    CefShutdown();
    return ret;
}

编译运行

QT集成CEF05-嵌入到QT窗体

问题: 内嵌浏览器窗口大小问题

当父窗体大小发生变化的时候,内嵌的窗体不随父窗体改变
QT集成CEF05-嵌入到QT窗体

解决这个问题思路就是:

  • 监听 MainWindow 的 resizeEvent 事件
  • 当窗口大小发生变化的时候,获取浏览器窗口句柄
  • 调用Windows API 函数 (::MoveWindow) 改变浏览器窗口位置,保持与MainWindow窗口一致。

那新的问题是 浏览器窗口的 窗口句柄 如何获取到?

找到 SimpleHandler 类,它 实现了 浏览器声明周期接口 GetLifeSpanHandler ,其中有个 OnAfterCreated(CefRefPtr<CefBrowser> browser) 回调方法,表示当浏览器窗口创建完成之后被回调,回调的时候传递了 CefBrowser对象。

在SimpleHandler 的实现中:

void SimpleHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
    CEF_REQUIRE_UI_THREAD();
    // 被加入到了browser_list_ 集合中
    browser_list_.push_back(browser);
}

我们现在的程序只创建了一个 CefBrowser 对象,所以此时可以在 SimpleHandler 类中再编写一个public的方法,用于获取这个浏览器窗口对象的窗口句柄:

// simple_handler.h 
// ... 其它部分省略
class SimpleHandler : public CefClient,
                      public CefLifeSpanHandler {
  // ... 其它部分省略
  // 这里为了方便,直接将实现写在了 simple_handler.h 这个头文件中了。
  HWND getBrowserWindowHandle() {
      if (!browser_list_.empty()) { //如果集合不为空
          // 获取集合中的第一个 CefBrowser元素 ,获取它的 CefBrowserHost 对象,然后再获取CefBrowserHost 对象中的 WindowHandle 即窗口句柄
          return  browser_list_.front()->GetHost()->GetWindowHandle();
      }
      return NULL;
  }
  // ... 其它部分省略                             
}

监听 MainWindow 的 resizeEvent 事件:

// mainwindow.cpp 文件
// ... 其它部分省略
void MainWindow::resizeEvent(QResizeEvent* event)
{
    if (SimpleHandler::GetInstance()) {
        HWND wnd = SimpleHandler::GetInstance()->getBrowserWindowHandle();
        if (wnd) {
            QRect qtRect = this->centralWidget()->rect();
            ::MoveWindow(wnd, qtRect.x(), qtRect.y(), qtRect.width(), qtRect.height(), true);
        }
    }
}

SimpleHandler 中提供了一个静态方法用来获取它的示例对象。如果它不为空,获取到对象后再调用刚才定义的 getBrowserWindowHandle() 方法获取到浏览器窗口的窗口句柄。

如果窗口句柄也不为空,则获取到当前QT窗体中的centralWidget 的矩形区域,然后将 浏览器窗口移动即可。

重新编译再次运行,发现浏览器窗口已经可以跟随QT窗口大小变化了。

上一篇:Qt-关闭进程4种方法


下一篇:C/C++ Qt 给ListWidget增加右键菜单