# 第一个 LarkSDK 工程

是时候开始真正开始编写 LarkSDK 程序了!在本篇教程中,我们将使用 LarkSDK 搭建一个简单的用户界面,在界面上我们将创建包括标签、按钮、文本框等常见的图形界面组件,利用弹性布局机制使得它们可以自适应窗口大小,同时借助信号槽机制,让用户点击按钮之后可以弹出一个简单的对话框。

如果你是一名熟练的 Qt 程序员,你会发现在 LarkSDK 中构建界面的代码十分容易理解。如果你从来没有用 C++ 代码构建过界面也不用担心,我们在设计 LarkSDK 设计的时候就充分考虑到了各种技术背景用户在设计图形界面时的关注点与编码习惯。

程序最终运行效果如下:

hello-larksdk-demo

# 创建工程目录

在前文中,我们已经讲解了如何搭建 LarkSDK 开发环境。下文假设你已成功在 Ubuntu 下搭建了基于 VSCode 和 CMake 的开发环境,同时假设 LarkSDK 已被正确安装到 /path/to/larksdk 路径下。

我们建立一个工程目录,假设为 ~/hello-larksdk,用于存放工程文件。工程目录的结构十分简单,只需要 CMakeLists.txtmain.cpp 两个文件。这也是我们设计 LarkSDK 的初衷:让一切保持简单。

工程目录结构如下:

hello-larksdk
  ├─ CMakeLists.txt
  └─ main.cpp

# 工程配置

我们借助 CMake 的能力进行工程配置。编辑 CMakeLists.txt 文件内容如下,注意 LarkSDK 的安装路径设置:








 


















cmake_minimum_required (VERSION 3.12)

project (hello-larksdk LANGUAGES CXX)

set (CMAKE_CXX_STANDARD 11)
set (CMAKE_CXX_STANDARD_REQUIRED ON)

set (LARKSDK_PATH "/path/to/larksdk")

set (CMAKE_BUILD_RPATH "${LARKSDK_PATH}/lib")

add_executable (hello-larksdk main.cpp)

target_include_directories (hello-larksdk PUBLIC "${LARKSDK_PATH}/include/lark-util")
target_include_directories (hello-larksdk PUBLIC "${LARKSDK_PATH}/include/lark-core")
target_include_directories (hello-larksdk PUBLIC "${LARKSDK_PATH}/include/lark-gui")
target_link_directories (hello-larksdk PUBLIC "${LARKSDK_PATH}/lib")

# 目前 LarkSDK 为静态库形式,故需要在这里手动引入部分系统库
# 后续将改为动态库实现,届时将不需在此引入系统库,只需引入 LarkSDK 相关库即可
target_link_libraries (hello-larksdk PUBLIC
    larkgui larkcore larkutil
    GL EGL
    pthread dl X11 wayland-client wayland-cursor xkbcommon
)

该 CMakeLists 文件描述了我们的工程只有唯一的一个源码文件 main.cpp,并合理的配置了构建时的引入目录和库目录等。

# 编写源代码

接下来编辑我们唯一的源码文件 main.cpp。代码并不长,因此我们先来看一看完整代码,再进行逐步讲解。

#include <lwindowapplication.h>
#include <lwindow.h>
#include <lcoremisc.h>
#include <lflexlayout.h>
#include <llabel.h>
#include <lbutton.h>
#include <ltextfield.h>
#include <lmessagebox.h>


int main()
{
    // 创建图形应用程序实例
    LWindowApplication app;

    // 创建窗体
    LWindow w;
    w.setTitle("Hello LarkSDK");

    // 为窗体的根组件设置为竖直方向的弹性布局,元素之间间隔 20 像素
    w.rootComponent()->setLayout(LFlexLayout(LFlexLayout::Vertical, LFlexLayout::OnAxisCenter, LFlexLayout::OffAxisCenter, 20));

    // 添加一个图片标签,图片资源来自于 LarkSDK 安装目录下的自带图片文件
    LLabel logo(LPixmap(LWindowApplication::assetDir() << "/lark-logo.png"), w.rootComponent());
    
    // 添加一个文本标签,设置字体大小和对齐方式
    LLabel helloText("欢迎使用合迅智灵 LarkSDK 开发框架!", w.rootComponent());
    helloText.setFont(LFont(24));

    // 添加一个容器组件,指定其尺寸并设置为水平方向的弹性布局,元素之间间隔 10 像素
    LComponent container(w.rootComponent());
    container.setSize(500, 32);
    container.setLayout(LFlexLayout(LFlexLayout::Horizontal, LFlexLayout::OnAxisCenter, LFlexLayout::OffAxisFill, 10));

    // 依次添加一个单行文本框和一个按钮到容器组件
    LTextField input("在此输入文本", &container);
    LButton btn("点我", &container);

    // 连接按钮点击信号到 Lambda 表达形式的槽函数
    // 点击按钮时将弹出消息对话框,对话框消息包含单行文本框的输入内容
    btn.clickSignal.connect([&w, &input]() { LMessageBox::information(&w, "对话框", LString("您输入了:") << input.text()); });

    // 显示窗体并运行程序
    w.show();
    return app.exec();
}

# 创建图形应用程序实例

每一个 LarkSDK 应用程序都需要手动建立一个应用程序实例对象。对于图形程序我们需建立 LWindowApplication 实例。如果你想构建一个非图形(命令行)应用程序,可以改为使用其基类 LApplication 实例。一个应用程序只允许建立一个实例。

int main()
{
    LWindowApplication app;

LApplication 实例在构造时会根据平台的不同处理一些初始化任务,包括读取配置文件、建立线程数据等,LWindowApplication 会额外处理图形界面相关的初始化任务,例如和平台窗口系统建立连接。

TIP

Qt 程序员对此会比较熟悉,类似 QApplicationQCoreApplication 的区别。

# 创建顶层窗体

接下来我们建立一个顶层窗体 w

    LWindow w;
    w.setTitle("Hello LarkSDK");

LarkSDK 设计了一套完整的基于窗体和组件的图形界面架构,顶层窗体 LTopWindowLWindow 为其别名)是一种最常见的窗体,通常用于桌面端程序。窗体默认会包含标题栏、边框和控制按钮等,用户可以进行诸如拖动窗体边框调整窗体大小、拖动标题栏以移动窗体之类的操作。

调用顶层窗体的 setTitle() 接口,可以设置其标题。如果操作系统支持,通常将在标题栏居中显示。

# 根组件与布局

接下来我们希望为窗体设置一个布局,便于添加到其中的组件可以从上往下居中排列,以实现我们期望的界面效果。

我们通过 rootComponent() 接口,访问顶层窗体 w根组件,并调用 setLayout() 接口,为其添加一个元素纵向排列、轴向居中、离轴方向居中、元素间隔 20 像素,无内边距的弹性布局。

    w.rootComponent()->setLayout(
        LFlexLayout(
            LFlexLayout::Vertical,
            LFlexLayout::OnAxisCenter,
            LFlexLayout::OffAxisCenter,
            20
        )
    );

LFlexLayout 描述了一个弹性布局,该类可以方便的通过组件类 LComponentsetLayout() 接口,以临时对象的形式传入。

根组件则是另一个重要的概念。因 LarkSDK 把窗体和组件的语义分开,而只有组件才可以添加组件以构成组件树。因此窗体的根组件是这样一个特殊的组件:一个窗体只可以拥有一个根组件,根组件的尺寸则永远与其所属窗体保持同步。这样我们只需以根组件作为容器继续添加更多组件即可构建界面。根组件可以理解为窗体和组件产生联系的桥梁,如下图所示。

root-component

TIP

事实上在 LarkSDK 中,窗体不一定必须有一个根组件。没有根组件的窗体必须自己负责自己的绘制。

LDrawableWindow 这一层基类的构造函数中,窗体可以选择自身的绘制模式。构造函数原型如下,注意第四个参数:





 




LDrawableWindow(
    int width,
    int height,
    LAbstractWindow *pParentWindow = nullptr,
    LDrawableWindow::DrawMode drawMode = LDrawableWindow::DirectDrawMode,
    LDrawableWindow::SurfaceType surfaceType = LDrawableWindow::NativeSurface,
    LComponent *pRootComponent = nullptr
)

其中 drawMode 参数可选 DirectDrawMode 直接绘制模式或 ComponentDrawMode 组件绘制模式。前者即代表直接绘制模式。LTopWindow 默认为组件绘制模式,组件绘制模式的窗体将自动创建根组件。

对于 Qt 程序员可能会比较熟悉,直接绘制模式类似于直接使用 QWindow 创建窗体。

现在我们已经创建了顶层窗体,并通过根组件配置了纵向居中的布局。下面我们可以开始添加组件。

# 添加图片与文本标签

我们以根组件作为父组件,添加两个 LLabel 标签组件到界面上,分别代表一张图片和一段文本。

    LLabel logo(
        LPixmap(LWindowApplication::assetDir() << "/lark-logo.png"),
        w.rootComponent()
    );

    LLabel helloText(
        "欢迎使用合迅智灵 LarkSDK 开发框架!",
        w.rootComponent()
    );
    helloText.setFont(LFont(24));

标签组件是最常用的界面组件之一,常用于显示一小段文本或展示一张图片,支持设置文本字体、大小、对齐方式等。第一个标签组件 logo 通过传入一个 LPixmap 位图对象(位图对象可以文件中读取位图数据),构建了一个图片标签;第二个标签组件则 helloText 通过传入字符串构建了文本标签,并立即通过 setFont() 接口设置了文本字体大小。

TIP

熟悉 Qt 的程序员可能会注意到我们添加组件的方式,无论容器存在布局与否,都可以直接在子组件构造函数参数中指定父组件的指针从而完成添加。同样的操作在 Qt 中,可能需要先创建布局对象,再通过布局对象(而非通过容器组件本身)的 addWidget() 接口来添加子组件。

我们认为 Qt 这样的语义设计是有待商榷的:

QWidget container;
QHBoxLayout layout;
container.setLayout(&layout);
QLabel labelA("A");
QLabel labelB("B");
layout.addWidget(&labelA);
layout.addWidget(&labelB);

在 LarkSDK 中,一切对子组件的增删操作均由父组件本身完成,布局仅仅是父组件的一个属性。这样一方面我们可以保持语义的统一,同时也方便直接匿名创建布局对象(而无需用一个额外的变量去记录布局对象,用于添加子组件)。

# 添加文本框与按钮

接下来我们添加最下方横向排列的文本框与按钮。这里需要用到嵌套布局。

    LComponent container(w.rootComponent());
    container.setSize(500, 32);
    container.setLayout(
        LFlexLayout(
            LFlexLayout::Horizontal,
            LFlexLayout::OnAxisCenter,
            LFlexLayout::OffAxisFill,
            10
        )
    );

    LTextField input("在此输入文本", &container);
    LButton btn("点我", &container);

首先我们添加一个 LComponent 组件作为容器,设置其尺寸,并设置其内部布局为元素横向排列、轴向居中、离轴方向填满、元素间隔 10 像素的弹性布局。

TIP

注意这里指定了容器的尺寸为固定值,因目前尚不支持容器组件自适应内部子组件尺寸的功能。

LComponent 为所有组件的基类,本身也可直接使用。直接使用时一般是作为容器。这里我们就构建了一个容器用于容纳单行文本框组件 LTextField 和按钮组件 LButton。由于布局的作用,它们将在容器内部中央横向排列,同时撑满容器纵向空间。

# 弹性布局回顾

现在我们已经为根组件设置了纵向弹性布局,内部嵌套了一个使用横向弹性布局的容器,成功构建出了我们想要的界面效果。事实上绝大多数布局需求都可以通过弹性布局互相嵌套来完成。为清晰说明弹性布局的效果,示意图如下:

flex-display

红色线条表示外层弹性布局,元素纵向排列,轴向居中,且互相间隔 20 像素;绿色线条表示从上往下数第三个容器中的内嵌弹性布局,元素横向排列,轴向居中,离轴方向填满容器的高度,且相互之间间隔 10 像素。

# 设置按钮点击交互

我们已经完成了用户界面的绘制,现在来做点有意思的事情:为按钮 btn 添加点击交互。

LarkSDK 支持信号槽机制。我们把按钮组件的点击信号 clickSignal 连接到指定的槽函数,即可简单的实现点击时触发槽函数的交互效果。

    btn.clickSignal.connect([&w, &input]() {
        LMessageBox::information(
            &w,
            "对话框",
            LString("您输入了:") << input.text()
        );
    });

槽函数可以是类的成员函数,也可以是普通函数或 Lambda 表达式。这里我们直接行内定义一个带捕获的 Lambda 表达式作为槽函数,将其连接到按钮 btn 的点击信号上。注意这里表达式捕获的是顶层窗体的引用 &w 和文本输入框组件的引用 &input

TIP

事实上,一切可以被 std::function 支持的对象都可以作为槽函数。

在 Lambda 函数体中,我们调用了 LMessageBox::information() 静态函数。该函数可以直接显示一个消息对话框,支持指定对话框的标题和文本。需要注意的是,消息对话框也是一种顶层窗体。但与前面我们创建的 LWindow 顶层窗体不同的是,它是模态的。模态窗体将屏蔽其所有者窗体的输入,强迫用户必须首先完成与自身的交互,常用于对话框等场合。

LMessageBox::information() 静态函数的本质是临时构建一个 LMessageBox 模态对话框(依次派生自 LDialogLWindow)对象。第一个参数传入构造模态对话框时必须指定的所有者窗体的地址(在这里就是顶层窗体 w 的地址)。

第二个参数表示消息框标题,第三个参数表示消息框内容,这里我们计划读取单行文本框 input 的当前文本,在前面加上“您输入了:”的字样,回显到消息框之中。

这里我们通过字符串 LString 对象的左移运算符 << 实现了字符串的连接。LString 是 LarkSDK 所提供的基于 Unicode 的字符串接口,可以方便的完成常见的字符串操作。例如这里除了使用左移运算符之外还可以使用 format() 格式化接口:

LString("您输入了:{}").format(input.text())

现在我们已经完成了按钮点击交互的配置。用户点击按钮 btn 后会弹出消息对话框,消息文本的一部分来自于单行文本框 input 中用户输入的内容。

# 显示窗体并进入事件循环

最后我们只需要将刚刚构建完成的顶层窗体 w 显示出来,再调用应用程序实例的 exec() 接口,就能看到运行的最终效果。

    w.show();
    return app.exec();
}

exec() 接口本质上是进入程序的主事件循环。进入事件循环后,LarkSDK 才可以接受来自操作系统的各种消息并进行响应。在不同的操作系统平台上,窗口系统也各有不同,不同的窗口系统拥有自己的事件监听与分发机制。不过作为用户的你不用担心这些,LarkSDK 内部已经完成了这些涉及跨平台的繁琐工作。

# 运行程序

最后我们借助 CMake 配置并运行程序。构建一个 build 目录用于存放编译输出:

$ mkdir build

目录结构如下:

hello-larksdk
  ├─ build
  ├─ CMakeLists.txt
  └─ main.cpp

然后我们进入 build 目录,配置、构建并运行程序:

$ cd build
$ cmake ..
$ make

最后运行程序:

$ ./hello-larksdk

最终效果如下:

hello-larksdk