CEF 桌面软件开发实战-2

在上一讲中,我向你详细介绍了 CEF 框架,以及如何搭建一个空白的 CEF 工程,如果我们接下去就讲解 CEF 内部的运作机制,比如多进程机制、多线程机制、 C++ 与 JavaScript 的交互机制等内容,那么可能会使你陷入“知识细节的海洋”中,而且平心而论 CEF 的这些知识细节多如牛毛,这样做难免会使大量读者弃坑。

因此,我们这节课程尽可能少地引入这些知识细节,让你在一个尽可能平缓的学习曲线上入门 CEF 开发框架,不仅如此,我还希望你能在学习完本节课程的过程中获得“激励”,看到自己开发的程序运行后的结果。为了达到这个效果,我做了最简 CEF 应用,这个应用只涉及到三个知识点:

  • 最简 CEF 应用入口程序的职责;
  • 最简 CEF 应用浏览器进程处理类的职责;
  • 最简 CEF 应用窗口代理类的职责。

接下来让我们进入正题,开启这段愉快的学习旅程吧。

一、入口程序

在上一讲中我们已经创建了一个名为 main.cpp 的程序文件,这是我们整个程序的入口文件,接下来我们先把这个文件的代码写好: 

#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;
}

这是整个应用程序的入口,应用启动后执行的第一段逻辑,我们在这段逻辑中完成了一些初始化工作、为不同的进程分派了不同的处理逻辑、开启了 CEF 的消息循环、在应用退出后释放 CEF 占用的资源等。

下面我们就详细解释一下这段代码的具体细节。

操作系统入口函数

wWinMain是操作系统指定的应用程序的入口函数。这个函数有 4 个参数,参数前有 In 修饰符表示该参数是必填的输入参数,有 In_opt 修饰符的意思是该参数是可选的输入参数。

这 4 个参数都是由操作系统传递给 wWinMain 方法的:

  • hInstance 是应用程序的实例句柄,也叫模块句柄;
  • hPrevInstance 没有实际意义,是老版本 Windows 系统的历史遗留产物;
  • lpCmdLine 是命令行参数;
  • nCmdShow 表示应用程序的窗口是最小化、最大化还是正常显示。

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

应用程序初始化

CefEnableHighDPISupport方法启用高分屏支持,如果不调用这个方法,你的应用程序在一些高分辨率的屏幕下将显示得很模糊。

CefMainArgs是 CEF 对应用程序实例句柄的包装类,用于多进程启动(关于多进程的内容我们在后面的章节再聊)。这里我们使用 hInstance 实例化了这个类的对象,名为:main_args 。

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

CEF 框架如何启动多个进程

CefExecuteProcess负责启动进程。这里需要详细介绍一下,一般我们启动一个 CEF 应用,你会发现任务管理器里有好几个进程: 

 这些进程中除了主进程是由用户启动的外,其他子进程都是 CEF 框架通过 CefExecuteProcess 方法启动的。

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

CefExecuteProcess 方法的第一个参数就是我们前面介绍的 CefMainArgs 对象,第二个参数可以是不同进程的业务处理对象,也可以为空。基于一切从简的原则,我们这里传递了一个空指针。第三个参数与 Chromium 沙箱有关,此处我们也没有设置。

CEF 特有的智能指针

接下来主进程会创建一个 App 对象(这个对象内部完成了什么工作我们稍后再讲),这个对象的指针被封装到一个CefRefPtr类型里了。

CefRefPtr 是一个智能指针类型。我们知道指针是 C/C++ 语言中一个重要的概念,开发者通过 new 实例化一个对象之后便得到了一个指针,此时开发者往往要把这个指针存起来,以便不用这个对象时能释放这个对象所占用的空间。然而开发者很多时候都不知道(或者忘记)什么时候这个指针指向的对象没用了(因为可能会有多个方法、类、模块、线程用到了这个对象)。这里的 CefRefPtr 智能指针就解决了这个问题,它内部有一个引用计数,持有这个指针的使用者越多,计数就越高,使用完了之后,计数也会相应地减少;当没有任何使用者之后,指针就会自动释放它指向的对象。

这与现代 C++ 中的智能指针并没有什么明显区别,CEF 框架大量使用了类似的智能指针,减轻了开发者的心智负担。

CEF 框架初始化及消息循环

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

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

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

二、浏览器进程入口

操作系统调用完程序的入口函数后,CEF 框架就通过其自身的消息循环机制接管了接下来的执行工作,在上一小节中我们提到了自定义的 App 对象,并且把这个对象传递给了 CEF 的CefInitialize方法,CEF 框架收到这个对象之后,会把浏览器进程的一些逻辑交给 App 对象执行,也就是说 App 对象就是我们浏览器进程的入口程序。先看一下它的头文件的代码:

#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宏指令告诉编译器这个文件只会被编译一次。这是现代 C++ 编译器新增的一个指令,这个指令出现之前 C++ 开发者都是通过如下方式来保证头文件不会被重复编译的。 

#ifndef _FileA
#define _FileA
// code 
#endif

这种方式虽然可以兼容古老的低版本编译器,但书写起来繁琐,编译时要预先分析文件内容,又非常低效,所以推荐使用#pragma once指令。

App 类的头文件中使用了宏 IMPLEMENT_REFCOUNTING ,这个宏为 App 类附加了一些特殊的方法,这些方法保证 App 类型的对象指针可以被 CefRefPtr 包裹(后面我们还会继续介绍 CefRefPtr 类型)。

浏览器进程的行为

App 类继承自 CefApp 和 CefBrowserProcessHandler 类,这两个基类提供了对浏览器进程的行为描述,比如:OnContextInitialized(浏览器进程的渲染线程初始化成功后被调用)、OnBeforeCommandLineProcessing (命令行参数被 CEF 和 Chromium 处理之前触发)等。我们在 App 类里只重写了 2 个基类方法,下面我们一个一个来介绍。

  • GetBrowserProcessHandler 方法返回浏览器进程的处理类实例指针,这里我们返回了 App 类的实例指针自身(也就是它自己)。因为实现代码很简短,我们就直接在头文件中完成了。这不违反 C++ 规范。浏览器进程内可能会有多个线程在执行,任何一个线程都有可能调用这个方法。

  • OnContextInitialized 方法在浏览器进程的主线程初始化成功后被调用,代表着浏览器进程已经初始化成功了。

窗口创建逻辑

我们在OnContextInitialized方法中创建了第一个窗口,这个方法代码在类的源码文件中完成,下面我们来看一下它的代码:

//App.cpp
#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() {
    CEF_REQUIRE_UI_THREAD();
    auto url = "https://www.zhihu.com/people/liulun";
    CefBrowserSettings settings;
    CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(nullptr, url, settings, nullptr, nullptr, nullptr);
    CefWindow::CreateTopLevelWindow(new WindowDelegate(browser_view));
}

这个方法的实现逻辑稍微多一些,我们一步步详细解释这段代码的执行逻辑。

  1. CEF_REQUIRE_UI_THREAD() 是一个宏,这个宏保证执行此方法的是浏览器进程的主线程。

  2. url 字符串变量用于存放应用的首页地址。

  3. settings对象, 是 CEF 配置对象,它的作用我们前文已经说过了,这里同样也是只用它默认的配置。

  4. browser_view是我们通过 CefBrowserView 类的 CreateBrowserView 静态方法创建了一个 BrowserView 对象。这个方法的第一个参数是一个 CefClient 类型的智能指针,它负责处理一切与页面相关的事件,比如下载、拖动、聚焦等,这里我们没有设置它的值,直接传递了一个空指针,要求 CEF 框架按照默认的行为处理页面相关的事件,在后续的章节我们会详细介绍它; url 字符串和 settings 对象作为第二个和第三个参数;第四个参数是一个用户自定义的附加信息对象,我们暂时用不到它;第五个参数是一个处理页面请求的对象,我们就使用 CEF 默认的请求处理机制,所以暂时也用不到它;第六个参数是 BrowserView 的代理对象,我们可以通过这个对象处理一些与 BrowserView 相关的事件,比如与该 BrowserView 关联的 Browser 对象创建成功的事件,为了简便,我们也没有用这个参数。

  5. 最后我们通过 CefWindow 类的 CreateTopLevelWindow 静态方法创建了一个窗口,这个方法只有一个参数,就是 CefWindowDelegate 对象。WindowDelegate 是我们自定义的一个类型,这个类型继承自 CefWindowDelegate 对象,所以它的实例也是 CefWindowDelegate 类型的对象(这是面向对象编程里的概念),我们通过这个 WindowDelegate 对象设置窗口的大小,聚焦窗口内的页面等,接下去我们就介绍它。

三、窗口代理对象

前文我们介绍了当浏览器进程的主线程初始化成功后,App 对象的 OnContextInitialized 方法会被执行,在这个方法的最后,我们通过 CreateTopLevelWindow 方法为 CEF 框架提供了一个窗口代理对象。 CEF 框架会把与窗口创建有关的逻辑交给这个对象来执行,接下来我们就看一下这个对象的头文件代码:

// WindowDelegate.h
 #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
#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 应用,点击 VisualStudio 的调试按钮,启动应用,看看是否得到了你想要的结果呢?

 

四、总结

在这一节我们通过创建一个精简的 CEF 应用程序,来带领你熟悉 CEF 框架的运作机制,可总结为如下:

  1. 操作系统调用应用程序入口函数;
  2. 入口函数初始化 CEF 框架并进入消息循环;
  3. CEF 框架初始化浏览器进程并把操作逻辑转交给 BrowserProcessHandler 对象;
  4. BrowserProcessHandler 对象创建 BrowserView 和 WindowDelegate 对象;
  5. 第一个窗口创建成功后,CEF 会通知 WindowDelegate 对象,并由它把 BrowserView 对象附加到窗口上。

虽然这个程序非常简单,但它还是涉及到了很多概念,比如 CEF 的视图处理类 BrowserView 、CEF 的窗口处理类 WindowDelegate 以及 CEF 的浏览器进程处理类 BrowserProcessHandler 等。我并没有详细介绍它们,这主要是为了避免一开始就介绍一大堆概念,劝退初学者,我会努力把 CEF 的学习曲线拉得平缓一些,在每个章节适度地把一些知识点传递给你。

尽管如此,可能还是会有些读者难以理解(比如,什么是面向对象,什么是继承),如果你遇到不明白的地方,请不要客气,向我提问吧。

虽然我们创建了第一个窗口,也让这个窗口加载了一个页面,但这个页面是一个互联网上的页面,对于一个桌面端 GUI 应用来说,这显然是不够的,迫在眉睫的问题就是:如何加载本地的页面呢?因此,我将在下一讲中继续介绍如何通过自定义 Scheme 让 CEF 加载本地页面。

五、示例代码下载

本节示例代码请通过如下地址自行下载:

gitee.com/horsejs_adm…

posted @ 2024-12-03 17:43  HelloMarsMan  阅读(955)  评论(0)    收藏  举报