QT作为C++下著名的跨平台软件开发框架,实现了一套代码可以在所有的操作系统、平台和屏幕类型上部署。我们前几篇文章讲解了如何构建一款基于CEF的简单的样例,但这些样例的GUI都是使用的原生的或者是控件功能不强大的CEF视图框架。本文将会重新开始,使用VS2019编写一款基于QT的并嵌入原生窗体的文章。
环境搭建
在本文中,我没有使用QtCreator进行项目搭建的工作,而是使用VS配合QT VS Tools类来完成项目的环境。在本文,假设你已经安装了QT,并且了解QT的相关知识。
在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文件结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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
的文件:
内容如下:
1 2 3 4 5 6 7 8 9 10 11 <?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 > <supportedOS Id ="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" /> <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #pragma once #include "include/cef_client.h" #include <list> class SimpleHandler : public CefClient, public CefLifeSpanHandler, public CefLoadHandler {public : explicit SimpleHandler () ; ~SimpleHandler (); static SimpleHandler* GetInstance () ; virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler () OVERRIDE { return this ; } virtual CefRefPtr<CefLoadHandler> GetLoadHandler () OVERRIDE { return this ; } virtual void OnAfterCreated (CefRefPtr<CefBrowser> browser) OVERRIDE ; virtual bool DoClose (CefRefPtr<CefBrowser> browser) OVERRIDE ; virtual void OnBeforeClose (CefRefPtr<CefBrowser> browser) OVERRIDE ; virtual void OnLoadError (CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, ErrorCode errorCode, const CefString& errorText, const CefString& failedUrl) OVERRIDE ; void CloseAllBrowsers (bool force_close) ; bool IsClosing () const { return is_closing_; }private : typedef std::list<CefRefPtr<CefBrowser>> BrowserList; BrowserList browser_list_; bool is_closing_; IMPLEMENT_REFCOUNTING (SimpleHandler); };
simple_handler.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 #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 ; 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 (); } } SimpleHandler::SimpleHandler (): 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 (); } }void SimpleHandler::OnLoadError (CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, ErrorCode errorCode, const CefString& errorText, const CefString& failedUrl) { CEF_REQUIRE_UI_THREAD (); if (errorCode == ERR_ABORTED) return ; 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)) { 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #pragma once #include "include/cef_app.h" class SimpleApp : public CefApp, public CefBrowserProcessHandler {public : SimpleApp (); virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler () OVERRIDE { return this ; } virtual void OnContextInitialized () OVERRIDE ;private : IMPLEMENT_REFCOUNTING (SimpleApp); };
simple_app.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #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进行初始化操作,对于该文件整体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <cef_app.h> #include "qtcefwindow.h" #include "stdafx.h" #include <QtWidgets/QApplication> #include "simple_app.h" 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) ; 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); QApplication a (argc, argv) ; const int result = init_qt_cef (argc, argv); if (result >= 0 ) { return result; } QtCefWindow w; w.show (); a.exec (); CefShutdown (); return 0 ; }
修改qtcefwindow窗体代码
qtcefwindow.h
为窗体添加私有成员:CefRefPtr<SimpleHandler>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #pragma once #include <QtWidgets/QMainWindow> #include "simple_handler.h" #include "ui_qtcefwindow.h" class QtCefWindow : public QMainWindow { Q_OBJECTpublic : QtCefWindow (QWidget *parent = Q_NULLPTR);private : Ui::QtCefWindowClass ui; CefRefPtr<SimpleHandler> simple_handler_; };
qtcefwindow.cpp
在构造函数中,处理关联qtcefwindow和SimpleHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include "qtcefwindow.h" #include <cef_request_context.h> #include "simple_handler.h" #include "stdafx.h" QtCefWindow::QtCefWindow (QWidget *parent) : QMainWindow (parent) { ui.setupUi (this ); 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); 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的效果吧。
运行程序,会发现报错:
1 2 3 4 5 6 7 --------------------------- 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