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文件结构如下:
代码语言:javascript复制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
的文件:
内容如下:
代码语言:javascript复制<?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
代码语言:javascript复制#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
代码语言:javascript复制// 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
代码语言:javascript复制#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
代码语言:javascript复制// 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进行初始化操作,对于该文件整体如下:
代码语言:javascript复制#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:
代码语言:javascript复制#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的效果吧。
运行程序,会发现报错:
代码语言:javascript复制---------------------------
QtCefDemo.exe - 系统错误
---------------------------
由于找不到 libcef.dll,无法继续执行代码。重新安装程序可能会解决此问题。
---------------------------
确定
---------------------------
对于这个问题,其实我们就是缺少运行时候的相关库文件,这里我们暂时先手动进行拷贝工作,以Debug环境为例,我们将资源文件拷贝到输出目录中:
然后将CefFilesbinDebug
中所有的文件拷贝到输出目录中:
当然,我们可以通过配置自动化脚本的方式,让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