CEF框架的基本认识

CEF框架的基本认识

CEF(Chromium Embedded Framework) 是一个基于Google Chromium的Web browser控件(桌面开发),旨在将 Chromium 浏览器的功能嵌入到其他应用程序中。它为开发者提供了一个轻量级的方式来集成 Web 内容浏览、HTML5 支持、JavaScript 执行等功能,从而使得开发者可以在桌面应用程序中嵌入网页浏览器或构建基于 Web 的应用程序。我们熟知的钉钉、学习通、网易云、微信等都是借助于CEF框架编写的。为了对CEF框架作最基本的了解,我们先自己写一个CEF框架Demo。

运行官方CEF的示例项目

我们主要进行3步准备。

CEF库以及Wrapper源码

CEF库下载地址

下载Standard版

下载完后是一个压缩文件。注意解压需要用压缩软件(如Bandizip等),使用原生Windows解压时间过长。

libcef库以及libcef_dll_wrapper静态库

  • cmake:该目录下存放了配置和构建以Windows作为编译环境的cmake配置文件,具体内容可以自行查看。
  • Debug和Release:这两个文件夹中,打开会看到已经编译好的CEF核心库文件
  • include:libcef本身提供的头文件以及wrapper会使用到的头文件。
  • libcef_dll:存放了libcef_dll_wrapper源码。
  • Resources:CEF作为内核的浏览器运行时需要用到的资源文件。
  • tests:存放了利用libcef、以及wrapper作为库来编写的浏览器Demo。其中,cefsimple编译出来的是一个简单的浏览器,而cefclient编译出来的是一个展示了cef许多API功能的exe。

使用 CMake 工具来编译 CEF

cmake下载地址

使用安装包更方便

打开cmake-gui.exe程序,在Where is the source code栏中选择源码所在目录,在下方的Where to build the binaries栏目中我们可以自定义任意位置,但是为了方便管理,我们选择在源码所在目录下增加一个build文件夹。

如下配置

Configure>Generate。cmake于是为我们生成了7个解决方案,存在于cef.sln文件中。

如图

使用VS编译

我们点击CMake中的Open Project(就是打开cef.sln),然后我们对simple或者client文件进行编译。

对simple编译

同时我们可以在simple_app.cc(或者client_app.cc)中修改url值来改变访问的地址。

双击exe文件访问

然后我们就可以在上面的文件中打开exe文件,将弹出所选url的页面。

基本原理认识

现在我们可以通过搭建一个全新的CEF工程来完成对这个程序更加深刻的理解。我们在Visual Studio中创建一个全新的C++项目,然后开始从0编写代码。

配置工程

  • “常规->C++语言标准”设置为:ISO C++17 标准 ( /std:c++17 ),这个配置可以使我们在工程中使用现代 C++ 的语言特性。
  • “C/C++ ->常规->附加包含目录”设置为: D:\Project\cef-projects\cef_binary_132.3.1+g144febe+chromium-132.0.6834.83_windows64 ,这个路径是你下载的 CEF 框架所在的路径。注意:在使用 CEF 框架前你必须已经成功编译了 CEF 的示例项目。
  • “C/C++ ->常规->预处理器定义”设置为如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
WIN32
_WINDOWS
__STDC_CONSTANT_MACROS
__STDC_FORMAT_MACROS
_WIN32
UNICODE
_UNICODE
WINVER=0x0601
_WIN32_WINNT=0x601
NOMINMAX
WIN32_LEAN_AND_MEAN
_HAS_EXCEPTIONS=0
PSAPI_VERSION=1
CEF_USE_SANDBOX
CEF_USE_ATL
_HAS_ITERATOR_DEBUGGING=0
_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS

这些预处理器定义大部分都是 Windows 平台和 CEF 框架要求的预处理器定义,最后一项是为了屏蔽 CEF 框架中低版本 C++ 警告的预处理器。

  • “C/C++ ->代码生成->运行库”设置为:多线程调试 (/ MTd ),这是为了适配 Chromium 的调试方式而设置的。
  • “链接器->输入->附加依赖项”设置为如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
D:\Project\cef-projects\cef_binary_132.3.1+g144febe+chromium-132.0.6834.83_windows64\build\libcef_dll_wrapper\Debug\libcef_dll_wrapper.lib
D:\Project\cef-projects\cef_binary_132.3.1+g144febe+chromium-132.0.6834.83_windows64\Debug\libcef.lib
D:\Project\cef-projects\cef_binary_132.3.1+g144febe+chromium-132.0.6834.83_windows64\Debug\cef_sandbox.lib
comctl32.lib
gdi32.lib
rpcrt4.lib
shlwapi.lib
ws2_32.lib
Advapi32.lib
dbghelp.lib
Delayimp.lib
OleAut32.lib
PowrProf.lib
Propsys.lib
psapi.lib
Shcore.lib

这是在编译、链接我们的程序时,使用 CEF 和 Windows 库的配置,其中 D:\Project\cef-projects\cef_binary_132.3.1+g144febe+chromium-132.0.6834.83_windows64 是你下载的 CEF 框架所在的路径。

  • “链接器->系统->子系统”设置为:窗口 ( /SUBSYSTEM:WINDOWS ),VisualStudio 创建空白项目时,默认是控制台项目,这里我们把它调整为窗口项目。
  • “清单工具->输入和输出->附加清单文件”设置为:$(ProjectDir)ceftest.manifest,这是为了让我们的工程生成的可执行程序兼容不同版本的 Windows 操作系统。设置好此配置之后,需要在工程的根目录下(与 main.cpp 在同一个目录)创建一个名为 ceftest.manifest 的文件,代码如下(此处的文件名你也可以按照自己的想法命名,不过注意配置里的文件名要和实际的文件名一致哦):
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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}"/>
<!-- 适用于 Windows 11 -->
<supportedOS Id="{A25EFA83-AFF6-4F06-9AC4-84E92AFC6F4B}"/>
</application>
</compatibility>
</assembly>

至此,这个全新的 C++ 工程就配置成功了,但现在还无法使用它,接下来我们就介绍如何为新工程准备资源。

资源准备

图中框起来的不要

把上面的资源复制到D:\Project\cef-projects\ceftest\x64\Debug中,因为只有这样这个新工程才能正确调用 CEF 框架提供的API。

入口程序

我们目标创建一个应用程序,打开就是我的博客首页。首先在解决方案中创建一个main.cpp文件作为主入口文件,后续再创建App.cpp等文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <windows.h>
#include "App.h"
//整个应用的入口函数
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow)
{
//CefEnableHighDPISupport(); 这个是启用高分屏支持的方法,现已不用手动调入
CefMainArgs main_args(hInstance);
CefSettings settings;
int exit_code = CefExecuteProcess(main_args, nullptr, nullptr);
if (exit_code >= 0) {
return exit_code;
}
CefRefPtr<App> app(new App());
CefInitialize(main_args, settings, app.get(), nullptr);
CefRunMessageLoop();
CefShutdown();
return 0;
}

下面我们来分别讲讲各段代码的用处:

wWinMain

这是 Windows 应用程序使用的标准入口点,尤其适用于基于 Windows API 的图形用户界面(GUI)应用程序。

声明部分

**int**:

  • 这个方法返回一个 int 类型的数字,操作系统会直接抛弃这个值。但如果有外部应用唤起你这个应用,那么这个返回值对于它来说可能是有意义的,一般返回 0 表示应用程序正常退出,返回其他值表示应用程序因异常而退出。

**APIENTRY**:

  • APIENTRY 是一个宏,通常在 Windows 编程中用于指定调用约定(calling convention)。调用约定定义了函数参数如何传递、返回值如何返回以及函数如何清理堆栈等。APIENTRY 实际上是 __stdcall,表示使用 Windows API 的标准调用约定。
  • 对于 Windows GUI 程序,这个约定用于确保正确的堆栈清理和传递参数。

**wWinMain**:

  • wWinMain 是 Windows 程序的入口点,通常用于 宽字符集(Unicode) 程序。如果你在程序中使用的是宽字符(如 wchar_t 类型),而不是传统的单字节字符(如 char),就会使用 wWinMain。如果是使用单字节字符集,则使用 WinMain

参数解析

参数前有 In 修饰符表示该参数是必填的输入参数,有 In_opt 修饰符的意思是该参数是可选的输入参数。

**_In_ HINSTANCE hInstance**:

  • hInstance 是当前应用程序的实例句柄。它是一个指向该应用程序的内存位置的标识符,在 Windows 中,实例句柄用于标识一个应用程序的运行实例。它通常在程序启动时由操作系统提供,后续可以用来访问程序的资源(如窗口、图标等)。

**_In_opt_ HINSTANCE hPrevInstance**:

  • hPrevInstance 是上一个实例的句柄。没有实际意义,是老版本 Windows 系统的历史遗留产物

**_In_ LPTSTR lpCmdLine**:

  • lpCmdLine 是指向命令行参数的指针。它包含传递给程序的命令行字符串(不包括程序名)。如果程序有命令行参数,它们将被传递给 lpCmdLine,你可以在程序中解析它们。例如,lpCmdLine 中可能包含用户指定的路径或其他配置参数。
  • LPTSTR 是一个指针,指向宽字符集的字符串(wchar_t*)。如果你使用的是宽字符集(Unicode),则此参数是一个宽字符字符串;如果使用的是单字节字符集(ASCII),则它是 char* 类型的字符串。

**_In_ int nCmdShow**:

  • nCmdShow是一个整数值,指定如何显示应用程序的主窗口。这个参数的值由操作系统传递,通常是控制窗口的可见性或状态。例如,nCmdShow 可以指示窗口是最小化、最大化还是普通显示。常见的值包括:
    • SW_SHOW:正常显示窗口
    • SW_MINIMIZE:最小化窗口
    • SW_MAXIMIZE:最大化窗口
    • SW_HIDE:隐藏窗口
应用程序初始化

CefMainArgs是 CEF 对应用程序实例句柄的包装类,用于多进程启动。这里我们使用 hInstance 实例化了这个类的对象,名为:main_args 。

CefSettings是 CEF 的配置对象,类似日志级别、调试端口等都是通过这个对象设置的,这里我们就全部使用默认值,后面涉及到具体的配置项之后再详细讲解。

CefExecuteProcess负责启动进程

假如我们已经写完了这个程序,我们在启动这个CEF的时候会发现有好几个进程:

多个进程

这些进程中除了主进程是由用户启动的外,其他子进程都是 CEF 框架通过 CefExecuteProcess 方法启动的。这是为什么呢?这是因为主进程启动后,执行到CefExecuteProcess时,此方法会立即返回 -1 。接下去主进程就会进入 CEF 的消息循环,在适当的时候主进程会以特殊的命令行参数多次启动你的可执行文件,这样就创建了多个子进程。子进程启动后也会执行到这个 CefExecuteProcess 方法,但子进程执行此方法会被阻塞(不会继续执行后面的逻辑),当子进程执行完它们的任务后,这个方法将返回一个大于等于 0 的值。也就是说子进程在第10行代码处就退出执行了,子进程不会执行 12~18 行代码。

CefRefPtr类中的智能指针

接下来主进程会创建一个 App 对象,这个对象的指针被封装到一个CefRefPtr类型里了。CefRefPtr用于管理 CEF 中对象的生命周期,它是 CEF 的引用计数智能指针,类似于 C++11 中的 std::shared_ptr,但是具有特定的设计,用于与 CEF 的引用计数机制配合使用。

在 CEF 中,大多数对象都通过引用计数来管理其生命周期。这意味着当一个对象被多个地方引用时,只有当最后一个引用被销毁时,资源才会被释放。这有助于避免内存泄漏和悬挂指针的问题。CefRefPtr 通过自动管理对象的引用计数,帮助开发者避免手动管理内存的复杂性。

CefInitialize方法对CEF框架初始化及消息循环

接下来主进程会执行CefInitialize方法,这个方法负责初始化 CEF 的浏览器进程处理类(注意:后文我们提到的浏览器进程与前文提到的主进程属于同一个进程)。这个方法的第一个参数仍然是我们前面创建的 CefMainArgs 对象,第二个参数是 CefSettings 对象,第三个参数就是 App 对象的指针,这里是通过 CefRefPtr 智能指针的 get 方法获取的。第四个参数与沙箱有关,这里我们依然置空。

CefRunMessageLoop负责开启 CEF 消息循环,这个方法会阻塞后面代码的执行,一直到应用程序的某个地方调用了 CefQuitMessageLoop 方法之后,这个方法才会退出执行。(CefQuitMessageLoop 方法会发射应用程序退出的消息,CefRunMessageLoop 方法会收到这个消息,收到这个消息后就退出方法了。)

CefShutdown方法会结束主进程,释放资源。最后应用程序退出。

浏览器进程入口

操作系统调用完程序的入口函数后,CEF 框架就通过其自身的消息循环机制接管了接下来的执行工作。在之前的主入口main中我们有App对象,并且把这个对象传递给了 CEF 的CefInitialize方法,CEF 框架收到这个对象之后,会把浏览器进程的一些逻辑交给 App 对象执行,也就是说 App 对象就是我们浏览器进程的入口程序

我们首先来看看App.h文件:

1
2
3
4
5
6
7
8
9
10
11
#pragma once
#include "include/cef_app.h"
class App : public CefApp, public CefBrowserProcessHandler
{
public:
App() = default;
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override { return this; }
void OnContextInitialized() override;
private:
IMPLEMENT_REFCOUNTING(App);
};

#pragma once 是一个编译指令,告诉编译器本头文件只会被包含一次。这样可以避免多次包含同一个头文件导致的重复定义问题。让我们来逐行分析一下这段代码:

class App : public CefApp, public CefBrowserProcessHandler:

  • 这行定义了一个名为 App 的类,并且它从 CefAppCefBrowserProcessHandler 这两个基类继承。

  • CefApp 是 CEF 应用程序的基类,它提供了对 CEF 应用程序生命周期的管理。

  • CefBrowserProcessHandler 是 CEF 中处理浏览器进程的类,它提供了浏览器进程相关的回调函数。

    因为 App 同时继承了这两个类,意味着它既是 CEF 应用程序的一个实例(CefApp),又能处理浏览器进程的事件(CefBrowserProcessHandler)。

App() = default:

  • 这是 App 类的构造函数定义。= default; 表示编译器会自动生成默认构造函数,这意味着没有任何自定义的初始化操作。如果需要添加构造函数的初始化行为,可以手动定义它。

CefRefPtr< CefBrowserProcessHandler > GetBrowserProcessHandler() override { return this; }:

  • 这个函数是 CefApp 类中的纯虚函数 GetBrowserProcessHandler 的重写(override)。

  • 该函数返回一个 CefRefPtr<CefBrowserProcessHandler>,也就是返回 this 指针(this 是当前类 App 的指针)。

  • CefRefPtr 是 CEF 中的智能指针,用于管理对象的生命周期,确保对象不会在使用期间被销毁。

    由于 App 同时继承了 CefBrowserProcessHandler,所以返回 this 指针是合法的,表示 App 也充当了浏览器进程的处理器。

void OnContextInitialized() override:

  • OnContextInitialized 是 CEF 中 CefBrowserProcessHandler 类的一个回调函数。当 CEF 完成初始化时,这个函数会被调用。

  • 在该函数内,通常会执行一些初始化代码,比如创建浏览器窗口、加载网页等。

    这个函数只是声明,在代码中可能会有相应的定义来实现实际的初始化逻辑。

IMPLEMENT_REFCOUNTING(App):

  • IMPLEMENT_REFCOUNTING 是 CEF 提供的宏,用来实现对象的引用计数功能。

  • CEF 使用引用计数来管理对象的生命周期。IMPLEMENT_REFCOUNTING(App) 会在类 App 中自动生成必要的代码,使得类 App 支持引用计数。

  • 引用计数机制可以确保对象在不再需要时被自动销毁,避免内存泄漏。

    这段代码使得 App 类对象可以通过 CEF 的引用计数系统来进行正确的内存管理。

总而言之这段代码定义了一个名为 App 的类,它继承自 CefApp 和 CefBrowserProcessHandler类,并且实现了 CefBrowserProcessHandler中的回调函数 OnContextInitialized。这个类主要用于处理 CEF 浏览器进程的初始化和生命周期管理。通过 CefRefPtr 管理对象的引用计数,确保内存管理的安全性。

窗口创建逻辑

我们在OnContextInitialized方法中创建了第一个窗口,让我们看看App.cpp文件内的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "App.h"
#include "include/cef_browser.h"
#include "include/views/cef_browser_view.h"
#include "include/views/cef_window.h"
#include "include/wrapper/cef_helpers.h"
#include "WindowDelegate.h"
//CEF主进程上下文环境初始化成功
void App::OnContextInitialized() { //表示调用App内的成员函数
CEF_REQUIRE_UI_THREAD();
auto url = "https://www.bayeeaa.top";
CefBrowserSettings settings;
CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(nullptr, url, settings, nullptr, nullptr, nullptr);
CefWindow::CreateTopLevelWindow(new WindowDelegate(browser_view));
}

CEF_REQUIRE_UI_THREAD():这是 CEF 的一个宏,确保后续的代码在 UI 线程中执行。UI 操作必须在主线程(UI 线程)上完成。

auto url = "https://www.bayeeaa.top";:定义了一个 URL 字符串,指定浏览器将要加载的网页地址。

CefBrowserSettings settings;:创建一个 CefBrowserSettings 对象,用于配置浏览器实例的设置。

CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(nullptr, url, settings, nullptr, nullptr, nullptr);:这行代码创建一个浏览器视图(CefBrowserView),并设置它的 URL、浏览器设置等。

  • nullptr 表示没有指定的参数,比如没有提供父窗口、没有指定特定的浏览器实例等。

CefWindow::CreateTopLevelWindow(new WindowDelegate(browser_view));:这行代码创建了一个顶级窗口,并将 WindowDelegate(一个窗口代理类的实例)与 browser_view 绑定。

  • WindowDelegate 类通常负责处理窗口的事件(如窗口大小调整、关闭等)。

窗口代理对象

窗口代理对象(WindowDelegate)通常是指一个负责管理和处理窗口事件的类。这些事件包括窗口的创建、大小调整、关闭、鼠标和键盘输入等。WindowDelegate 不是 CEF 的核心组件之一,但它通常作为一个自定义类出现在 CEF 的应用程序中,用来简化与窗口相关的操作和事件处理。当浏览器进程的主线程初始化成功后,App 对象的 OnContextInitialized 方法会被执行,在这个方法的最后,我们通过CreateTopLevelWindow 方法为 CEF 框架提供了一个窗口代理对象。 CEF 框架会把与窗口创建有关的逻辑交给这个对象来执行

所以,在 CEF 中,窗口本身并不直接处理用户交互或者窗口系统事件。相反,它依赖于代理对象来执行这些任务。通过创建一个 WindowDelegate 对象,可以自定义和实现窗口的行为,处理和响应窗口的生命周期事件。

WindowDelegate.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#pragma once
#include "include/views/cef_window.h"
#include "include/views/cef_browser_view.h"
class WindowDelegate : public CefWindowDelegate
{
public:
explicit WindowDelegate(CefRefPtr<CefBrowserView> browser_view) : browser_view_(browser_view) {};
void OnWindowCreated(CefRefPtr<CefWindow> window) override;
void OnWindowDestroyed(CefRefPtr<CefWindow> window) override;
CefRect GetInitialBounds(CefRefPtr<CefWindow> window) override;
WindowDelegate(const WindowDelegate&) = delete;
WindowDelegate& operator=(const WindowDelegate&) = delete;
private:
CefRefPtr<CefBrowserView> browser_view_;
IMPLEMENT_REFCOUNTING(WindowDelegate);
};

WindowDelegate 类继承自 CefWindowDelegate ,它的构造函数接收一个 CefBrowserView 类型的智能指针,并把这个智能指针存放到 browser_view_ 私有变量中,以备后续使用。

与 App 类一样,它也使用了 IMPLEMENT_REFCOUNTING 宏,除此之外,它还删除了拷贝和赋值操作(注意头文件中的两个 “…= delete” 语句,这部分内容我们将在下一节课进行详细讲解)。

这个类实现了父类的三个方法,CEF 框架会在适当的时机调用这三个方法,一般我们可以把它理解为窗口生命周期内的事件,它们分别是:设置窗口位置和大小事件、窗口创建成功事件、窗口销毁成功事件

WindowDelegate.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
#include "WindowDelegate.h"
#include "include/cef_app.h"
#include "include/views/cef_display.h"
//窗口创建成功
void WindowDelegate::OnWindowCreated(CefRefPtr<CefWindow> window) {
window->AddChildView(browser_view_);
window->Show();
browser_view_->RequestFocus();
window->SetTitle(L"这是我的窗口标题");
//window->CenterWindow(CefSize(800, 600));
}
//窗口销毁成功
void WindowDelegate::OnWindowDestroyed(CefRefPtr<CefWindow> window) {
browser_view_ = nullptr;
CefQuitMessageLoop();
}
//设置窗口位置和大小
CefRect WindowDelegate::GetInitialBounds(CefRefPtr<CefWindow> window) {
CefRefPtr<CefDisplay> display = CefDisplay::GetPrimaryDisplay();
CefRect rect = display->GetBounds();
rect.x = (rect.width - 800) / 2;
rect.y = (rect.height - 600) / 2;
rect.width = 800;
rect.height = 600;
return rect;
}

设置窗口位置和大小

当 App 类 CreateTopLevelWindow 方法被执行后,CEF 框架将创建一个系统窗口,创建这个窗口之前,CEF 框架会调用GetInitialBounds方法,在这个方法中我们做了如下几个工作。

  1. 设定窗口的尺寸为宽 800 像素,高 600 像素。
  2. 通过CefDisplay 类的静态方法GetPrimaryDisplay 获取到了用户的主屏幕信息。
  3. 根据主屏幕信息及设定的窗口尺寸计算出窗口位于屏幕正中间时窗口的坐标。
  4. 通过 CefRect 结构把窗口的坐标及尺寸返回给 CEF 框架。

我们可以在窗口创建成功之后再通过窗口对象的 CenterWindow 方法来把窗口设置到屏幕正中间(同时也可以设置窗口尺寸),但这显然不如在窗口创建之初就明确窗口的位置和尺寸更高效。如果开发者不通过 GetInitialBounds 方法设置窗口的尺寸,还可以通过重写基类的 GetPreferredSize方法来设置窗口尺寸。 CEF 的示例项目就是这么做的,但我认为还是在GetInitialBounds 方法中完成这项工作比较好。

窗口创建成功事件

当 App 类 CreateTopLevelWindow 方法被执行后,CEF 框架将创建一个系统窗口,当这个窗口成功创建完成后, OnWindowCreated 被调用,我们在这个方法里把 App 类里创建的 BrowserView 对象添加到了这个窗口中( window->AddChildView ),然后让这个窗口显示出来( window->Show ),最后用户焦点被聚焦在 BrowserView 上( browser_view_->RequestFocus ),最后一行注释掉的代码就是把窗口移动到主屏幕中央的代码。

窗口销毁成功事件

当窗口被销毁后(可能是用户点击了窗口的关闭按钮,也可能是代码逻辑触发了窗口关闭的方法),OnWindowDestroyed 方法被执行,此处我们把 browser_view_ 指针置空。接着我们执行了 CefQuitMessageLoop 方法,这个方法会在 CEF 的消息循环中插入一个退出消息,CEF 框架收到这个消息后,会退出消息循环,清理资源,退出应用。


CEF框架的基本认识
https://bayeeaa.github.io/2025/01/26/CEF基本认识/
Author
Ye
Posted on
January 26, 2025
Licensed under